KeyboardWorm and MouseWorm

After talking things through with Wendy yesterday, I moved on from KeyBoardScalingCircleGrid to KeyboardSnake. Wendy pointed out the multicoloured background wasn’t necessary for this version – so I could concentrate on making the interaction work with a single colour for the snake. I decided to change the name to the slightly more friendly KeyboardWorm.

I began by creating a WormSegment object:

function WormSegment(aColour,aRadius){ //WormSegment object
 this.radius = aRadius;
 this.position = createVector(0.5,0.5); //start in centre of screen
 this.colour = aColour;

this.display = function(){
 var translatedX = this.position.x * windowWidth;
 var translatedY = this.position.y * windowHeight;
 fill(this.colour);
 ellipse(translatedX, translatedY, this.radius, this.radius);
 }
}

Then created an array of these segments to make up a worm:

var worm = []; //array of WormSegment objects
var wormLength = 30; //number of segments of the worm

and

function setup() {
 createCanvas(windowWidth,windowHeight); //make a fullscreen canvas, thanks to: http://codepen.io/grayfuse/pen/wKqLGL
 noStroke(); //no outlines, just filled shapes
 colorMode(HSB, 100);// Use HSB with scale of 0-100, see https://p5js.org/reference/#/p5/color
 var segmentColourMaxBrightness = 100;
 var segmentColourMinBrightness = 80;
 var segmentColourBrightnessRatio = (segmentColourMaxBrightness-segmentColourMinBrightness)/wormLength;
 var segmentColour = color(random(100),50,segmentColourMaxBrightness, 100); //random hue, saturation 50%, brightness 100%, alpha 100%
 var segmentMaxRadius = 100;
 var segmentMinRadius = 70;
 var segmentRadiusRatio = (segmentMaxRadius-segmentMinRadius)/wormLength;
 var segmentRadius = segmentMaxRadius;

for (var i=0; i < wormLength; i++) {
 worm.push(new WormSegment(segmentColour, segmentRadius));
 segmentRadius -= segmentRadiusRatio;
 segmentColour = color(hue(segmentColour), saturation(segmentColour), brightness(segmentColour)-segmentColourBrightnessRatio, alpha(segmentColour));
 }
 console.log("The length of the worm is " + worm.length);
}

I changed my draw() function to the following:

function draw() {
 background(255); //white background
 updateWorm();
 drawWorm();
}

Drawing the worm was pretty straight forward, after I realised that I had to draw the segments in reverse order so that the “head” of the worm drew last:

function drawWorm(){
 //draw the first segment of the worm last so that the shading looks correct
 for (var i = (worm.length-1); i > 0; i--) {
 worm[i].display();
 }
}

Most challenging was updating the worm:

function updateWorm(){
 seekWormTowardsKey(key);

//starting at back of the worm, copy the previous worm segments position onto the current segments position
 for (var i = (worm.length-1); i > 0; i--) {
 worm[i].position.x = worm[i-1].position.x;
 worm[i].position.y = worm[i-1].position.y;
 //had to copy both values - not the reference, worm[i].position = worm[i-1].position doesn't work
 }
}

As I noted in the comments – just setting the position as a reference didn’t work – I had to set each position x and y value manually – this was a nasty bug to track down, after two hours of drawing and redrawing my segment shifting code I finally realised my error.

Seeking the worm towards the key was a matter of checking to make sure that the key was valid, then easing towards the virtual key position – code that I had already written for the KeyboardScalingCircleGrid Reactickle.

function seekWormTowardsKey(aKey){
 var lowerCaseKey = key.toLowerCase(); //key is a system variable via https://p5js.org/reference/#/p5/key, toLowerCase via http://www.w3schools.com/jsref/jsref_tolowercase.asp

if(allTheKeys.includes(lowerCaseKey)){
 //if the key is a valid one, then seek the worm towards it
 seekWormTowardsPosition(getCanvasPositionFromKey(lowerCaseKey));
 }
}

function seekWormTowardsPosition(relativeSeekPosition){
 var easing = 0.05;
 //move the head of the worm a bit closer... via https://processing.org/examples/easing.html
 var dx = relativeSeekPosition.x - worm[0].position.x;
 worm[0].position.x += dx * easing;
 var dy = relativeSeekPosition.y - worm[0].position.y;
 worm[0].position.y += dy * easing;
}

You can try the KeyboardWorm online, as well as a MouseWorm version that I made as a bonus, which also works on Android or iOS mobile devices.

Storyboards for the interaction

Thanks to the wonderful Tom Jennings, the project has some storyboards to help explain how to interact with project:

Deep receives a photo message from her friend Zen showing her posing in front of an amazing sculpture that appears to hang in the sky above.
Deep receives a photo message from her friend Zen showing her posing in front of an amazing sculpture that appears to hang in the sky above.
Intrigued, Deep accessed the website that Zen pointed her to.
Intrigued, Deep accessed the website that Zen pointed her to.
After the website loads, Deep finds she can pan her smart phone around like a window onto the world and even use her finger to drag and change the sculpture in real time.
After the website loads, Deep finds she can pan her smart phone around like a window onto the world and even use her finger to drag and change the sculpture in real time.
Deep finds an angle she likes and shares it with her father via a text message.
Deep finds an angle she likes and shares it with her father via a text message.
Deep’s father receives the message from Deep, an image and an instruction how to interact with the project via a feature phone.
Deep’s father receives the message from Deep, an image and an instruction how to interact with the project via a feature phone.
After checking on Google Maps, Deep’s father sends his longitude and latitude to the special number that Deep shared with him.
After checking on Google Maps, Deep’s father sends his longitude and latitude to the special number that Deep shared with him.
Deep’s father receives a text message in response, showing the sculpture in the sky above his local area. He likes it so much he shows it to Deep’s grandmother.
Deep’s father receives a text message in response, showing the sculpture in the sky above his local area. He likes it so much he shows it to Deep’s grandmother.
In a nearby square, the British Council projects the sculpture on a large wall. Locals can see the sculpture change in real time as users touch it on their smart phones.
In a nearby square, the British Council projects the sculpture on a large wall. Locals can see the sculpture change in real time as users touch it on their smart phones.

Reporting a bug properly, making KeyboardScalingCircleGrid

After tweeting about the multitouch bug to the author of p5.js, I received the following reply:

I therefore filed the bug on the p5.js GitHub.

Wendy forwarded me a video that she had shot of someone using the original version of Reactickles that I am now in the process of porting to the web. I’ve embedded it below:

The three Reactickles that I am aiming to port initially are at the following points in the video:

  1. 2:41, which I am calling KeyboardScalingCircleGrid.
  2. 3:57, which I am calling KeyboardBouncingCircleGrid.
  3. 0:37, which I am calling KeyboardSnake.

keyboardscalingcirclegridgrab

So lets start with the development of KeyboardScalingCircleGrid. I began by duplicating the code I wrote yesterday (KeyboardToScreen) and renaming the folder to KeyboardScalingCircleGrid.

I knew that I would have to create an array of ScalingCircles and instantiate them with certain values, so I had a look at the p5.js examples page to see if there were any sketches that might be useful. Sure enough, the Array of Objects example and the Objects 2 example looked perfect.

I started by creating a new ScalingCircle object:

function ScalingCircle(aKey, aCircleRadius){ //ScalingCircle object
 this.key = aKey;
 this.circleRadius = aCircleRadius;
 this.position = createVector(-1,-1);
 this.position = getCanvasPositionFromKey(aKey);
 this.colour = color(random(100),50,100); //random hue, saturation 50% and brightness 100%

this.display = function(){
 var translatedX = this.position.x * windowWidth;
 var translatedY = this.position.y * windowHeight;
 fill(this.colour);
 ellipse(translatedX, translatedY, this.circleRadius, this.circleRadius);
 }; //don't forget to close your method!
}

And creating an array of those objects inside the setup method of the sketch:

var characterSize = 50;
var circles = []; //array of ScalingCircle objects
var allTheKeys = "1234567890qwertyuiopasdfghjklzxcvbnm";
var circleRadius = 100;

function setup() {
 createCanvas(windowWidth,windowHeight); //make a fullscreen canvas, thanks to: http://codepen.io/grayfuse/pen/wKqLGL
 textSize(characterSize);
 colorMode(HSB, 100);// Use HSB with scale of 0-100, see https://p5js.org/reference/#/p5/color
 for (var i=0; i < allTheKeys.length; i++) {
 circles.push(new ScalingCircle(allTheKeys[i],circleRadius));
 }
 console.log("The size of the circles array is " + circles.length);
}

Finally, I looped through the array of circles inside the draw method of the sketch:

function draw() {
 background(255); //white background
 noStroke();
 for (var i=0; i<circles.length; i++) {
 circles[i].display();
 }
}

This resulted in the following output:

2016_11_22_firstgridofcircles_scalingcircle

The next step is to add scaling when keyboard buttons are pressed.

I added a scaleUp() method to the ScalingCircle, and changed the display method to ease towards the new targetCircleRadius (The Processing Easing example was useful for this):

function ScalingCircle(aKey, aCircleRadius){ //ScalingCircle object
 this.key = aKey;
 this.actualCircleRadius = aCircleRadius;
 this.targetCircleRadius = aCircleRadius;
 this.position = createVector(-1,-1);
 this.position = getCanvasPositionFromKey(aKey);
 this.colour = color(random(100),50,100); //random hue, saturation 50% and brightness 100%

this.display = function(){
 var differenceInRadius = this.targetCircleRadius - this.actualCircleRadius;
 var changeThisFrame = differenceInRadius*easing;
 this.actualCircleRadius += changeThisFrame;
 var translatedX = this.position.x * windowWidth;
 var translatedY = this.position.y * windowHeight;
 fill(this.colour);
 ellipse(translatedX, translatedY, this.actualCircleRadius, this.actualCircleRadius);
 }; //don't forget to close your method!

this.scaleUp = function(){
 this.targetCircleRadius = this.actualCircleRadius+10;
 }
}

Then added code to check which button was pressed, and to call the scaleUp() method on the correct ScalingCircle in the circles array:

function keyTyped(){
 var lowerCaseKey = key.toLowerCase(); //key is a system variable via https://p5js.org/reference/#/p5/key
 for (var i=0; i<circles.length; i++) {
 if(lowerCaseKey == circles[i].key){
 circles[i].scaleUp();
 }
 }
 return false; //https://p5js.org/reference/#/p5/keyTyped preventing default behaviour
}

This resulted in a pleasing scaling animation, but it wasn’t quite as fast as the video, so I altered the easing ratio to 0.3 from 0.1, as well as setting a fixed number for the target radius of each circle – initially 100 pixels, then 200 pixels on keypress. Finally, I changed the colour of the circle to be 50% transparent to match the blending effect that was visible on the Reactickles 1 demonstration video.

2016_11_22_alphacircles_scalingcircle

However, in the video I could see that the circles not only scale up, but scale back to their original size – and worse than that they don’t scale up in a linear way, but seem to “bounce” around their full size – overshooting initially and then scaling back. From previous experience in Actionscript and C++ I knew of the existence of Robert Penner’s Easing functions, and that it was very likely that they had already been implemented in p5.js. I Googled “easing functions p5.js” and found p5.ijeoma.js:

 A p5.js addon for ijeoma.js, a JS library for creating animations.

I downloaded the library and added it and its dependencies to my libraries folder. Looking at the examples and documentation I started by creating a tween for the scaling up of circles:

function ScalingCircle(aKey){ //ScalingCircle object
 this.key = aKey;
 this.startCircleRadius = 100;
 this.endCircleRadius = 200;
 this.circleRadius = this.startCircleRadius; //start with the start
 this.scaleUpDuration = 0.5; //take half of a second to scale up
 this.scaleDownDelay = this.scaleUpDuration; //wait until the scale up is down to scale down
 this.scaleDownDuration = 0.25; //take quarter of a second to scale down
 this.position = createVector(-1,-1);
 this.position = getCanvasPositionFromKey(aKey);
 this.colour = color(random(100),50,100,50); //random hue, saturation 50% and brightness 100%, alpha 50%

this.display = function(){
 var translatedX = this.position.x * windowWidth;
 var translatedY = this.position.y * windowHeight;
 fill(this.colour);
 ellipse(translatedX, translatedY, this.circleRadius, this.circleRadius);
 }; //don't forget to close your method!

this.scaleUpAndThenDown = function(){
 //syntax for tweens is createTween(object property, end, duration, [delay], [easing])
 //see https://github.com/ekeneijeoma/p5.ijeoma.js
 var scaleUpTween = createTween('this.circleRadius', this.endCircleRadius, this.scaleUpDuration).easing(Quad.In).play();
 var scaleDownTween = createTween('this.circleRadius', this.startCircleRadius, this.scaleDownDuration, this.scaleDownDelay).easing(Quad.Out).play();
 }
}

Unfortunately, this didn’t work, resulting in the JavaScript errors:

[Log] p5 had problems creating the global function "frames", possibly because your code is already using that name as a variable. You may want to rename your variable to something else. (p5.js, line 9429)
[Log] p5 had problems creating the global function "stop", possibly because your code is already using that name as a variable. You may want to rename your variable to something else. (p5.js, line 9429)
[Log] The size of the circles array is 36 (sketch.js, line 14)
[Warning] Only numbers, p5.colors and p5.vectors are supported. (p5.ijeoma.js, line 142)
[Error] TypeError: undefined is not an object (evaluating 'this._properties[i].update')
 _updateProperties (ijeoma.js:1170)
 dispatchChangedEvent (ijeoma.js:1297)
 seek (ijeoma.js:194)
 play (ijeoma.js:148)
 scaleUpAndThenDown (sketch.js:59:122)
 keyTyped (sketch.js:31)
 _onkeypress (p5.js:16261)
 (anonymous function)

I duplicated the code into an example, Tweeted at the developer, filed a bug on the library GitHub and rolled back to my previous code.

I decided to try getting elements to scale up and then down, before worrying about “bouncing”.

function ScalingCircle(aKey){ //ScalingCircle object
 this.key = aKey;
 this.circleBigRadius = 200;
 this.circleSmallRadius = 100;
 this.circleRadius = this.circleSmallRadius;
 this.position = createVector(-1,-1);
 this.position = getCanvasPositionFromKey(aKey);
 this.colour = color(random(100),50,100,50); //random hue, saturation 50% and brightness 100%, alpha 50%
 this.millisToScaleUp = 50;
 this.millisToScaleDown = 200;
 this.startScale = 0;
 this.endScale = 0;
 this.scaling = false;

this.display = function(){
 if(this.scaling){
 this.scale();
 }
 var translatedX = this.position.x * windowWidth;
 var translatedY = this.position.y * windowHeight;
 fill(this.colour);
 ellipse(translatedX, translatedY, this.circleRadius, this.circleRadius);
 }; //don't forget to close your method!

this.scale = function(){
 var now = millis();
 var millisElapsed = now-this.startScale;

if(millisElapsed < this.millisToScaleUp){
 var howFarAlongScaleUp = millisElapsed/this.millisToScaleUp;
 this.scaleUp(howFarAlongScaleUp);
 }else{
 var howFarAlongScaleDown = (millisElapsed-this.millisToScaleUp)/this.millisToScaleDown;
 this.scaleDown(howFarAlongScaleDown);
 }

if(now >= this.endScale){
 this.scaling = false;
 }
 }

this.scaleUp = function(howFarAlongScale){
 var differenceInRadius = this.circleBigRadius - this.circleSmallRadius;
 var newRadius = this.circleSmallRadius+(howFarAlongScale*differenceInRadius);
 this.circleRadius = newRadius;
 }

this.scaleDown = function(howFarAlongScale){
 var differenceInRadius = this.circleBigRadius - this.circleSmallRadius;
 var newRadius = this.circleBigRadius-(howFarAlongScale*differenceInRadius);
 this.circleRadius = newRadius;
 }

this.scaleUpandDown = function(){
 this.scaling = true;
 this.startScale = millis();
 this.endScale = this.startScale+this.millisToScaleUp+this.millisToScaleDown;
 }
}

This resulted in a scaling up and down, but without the bounce that I could see in the video.

Working towards porting a Reactickle by translating keyboard presses to the screen

After checking in with Wendy on the project, I started porting the first Reactickle to the web. I’ve christened this Reactickle KeyboardSnake. It’s a very simple interaction on the surface – pressing any of the keyboard makes a multi-coloured snake-like circle form seek that position on the screen – with each of the keys of the keyboard corresponding to a different relative position on the screen canvas.

To start development I wanted to make the mapping between screen and keyboard easy to see, so I created an intermediate piece of code called KeyboardToScreen.

I need to map the positions of the four rows of keys from their positions on the keyboard to their relative positions on screen:

  1. The first row of keys are the numbers, from 1-0. There are 10 keys in this row.
  2. The second row of keys are the letters qwertyuiop. There are 10 keys in this row.
  3. The third row of keys are the letters asdfghjkl. There are 9 keys in this row.
  4. The third row of keys are the letters zxcvbnm. There are 7 keys in this row.

I needed to make a new function that would take any keyboard press and return a vector comprising two floats – the first being the relative x position of the key pressed to canvas space, and the second being the relative y. The p5.js wiki page on JavaScript basics was very helpful on how to make new functions and return values, with the handy p5.vector class perfect to store the key’s canvas position.

The key variable stores the most recently pressed keyboard character, so that was perfect to use to call my custom function. I needed to make extensive use of the switch statement in order to deal with all the keyboard presses I might have to deal with – not forgetting their uppercase variants in the case of characters.

Try the demo of KeyboardToScreen. Source code is also available.

Completing the p5.js tutorials

In my previous post, I completed the following tutorials from the p5.js website:

  1. Hello p5.js
  2. Get Started
  3. p5.js overview

First, I fixed smart quotes in said previous post, then got on with the next tutorial, p5.js and Processing aka Processing transition.

Several parts of the tutorial were particularly interesting or relevant to the Reactickles 3 project:

  1. The ability of p5.js to take multitouch input, rather than just from a single mouse or trackpad. To test this out, I created a multitouch demo, “TouchDemonstration“. Unfortunately, it didn’t work on my laptop, so to test it I wanted to put it on the web proper. I knew GitHub had a hosted pages capability, so I followed the tutorial and created pages for this project. I had to make the circles sufficiently large to be seen under my fingers, and scaled my canvas to the full size of the window, but even then they didn’t seem to be picked up as multi touches rather than single ones. I posted on the forum and am awaiting some tips. I noted that the developer of p5.js seems to be changing the things are working with multitouch in the next release.
  2. The difference between Global and Instance mode, and via that page:
  3. The concept of Closures, and why they are so powerful:

    A closure is a special kind of object that combines two things: a function, and the environment in which that function was created. The environment consists of any local variables that were in-scope at the time that the closure was created.

In order to stop the duplication of code from one folder from the other, something that is sure to create bugs, I moved all my source code into my GitHub pages (or docs) folder on the GitHub for the project, with one unified libraries folder for p5.js and other JavaScript libraries.

Final changes to the demonstration files for user testing

2016_11_10_macbookprograb

Dietrich Ayala added click and drag functionality to the project, via the Click Drag component, so after merging and pulling his changes to my local project folder, I installed it via:

npm install aframe-click-drag-component --save

I also edited my demonstration page for the project to make it much clearer which pages should be used for user testing in India next week.

Finally, I moved the virtual mountain a bit higher in the virtual sky to encourage users to look up.

Creating the release versions of the demo’s for user testing

User testing in India begins next week via the British Council so this week is my last chance to tweak the two demonstration files I’ll be submitting, one designed for iPhones and PCs without cameras and one designed for Android phones and Laptops with cameras.

On a Google Pixel phone the Mountain component isn’t particularly speedy, so I started by duplicating the component and trying to edit it to reduce it’s resolution. I also created a new pug file for the faster component and added it to my mainForRelease.js file so that Browserify would bundle it properly.

After a chat with the author of the component, Kevin Ngo, on the A-Frame Slack, he advised me to submit a pull request with the changes I required, and he would fold them into the main branch. He also supplied me with a tutorial on how to submit a pull request. I removed all my previous work and started by forking Kevin’s K-Frame repository by clicking the fork button on it’s GitHub. A new fork now existed on my GitHub. I then cloned it to my local machine:

git clone https://github.com/JGL/kframe.git

Then I moved into the folder itself, and created a new branch called mountain-variable-size:

cd kframe
git branch mountain-variable-size
git checkout mountain-variable-size

I then ran:

npm install

To install dependencies locally, then:

npm run dev

To ensure I had a local working copy. I ran:

git config --global core.editor "nano"

To make sure I was using the simple nano editor for any git editing that might happen. I then edited kframe/components/mountain/index.js to the changes that I wanted, which were all to do with reducing the number of width and height segment in it’s internal PlaneBuffer. I then added it to my branch, ignored the generated kframe.js and kframe.min.js files, committed my changes and pushed the changes to my fork on GitHub.

git add components/mountain/index.js 
git checkout dist/kframe.js
git checkout dist/kframe.min.js
git commit -m "Added world-depth and world-width to Mountain component"
git push --set-upstream origin mountain-variable-size

Kevin then merged the changes into his Master, and pushed the changes to npm, which meant I could just run:

npm update aframe-mountain-component

To get all the changes in my project. Creating a lower resolution mountain with the following pug code made everything run on the Pixel far faster:

a-mountain(id='mountain' color='rgb(128,0,128)' shadowColor='rgb(255,165,0)' world-depth='128' world-width='128' position='0 2000 0' material-side-modifier-mountain)

You can try the faster demo, and compare it to the slower version. I also added these changes to the release AR and the release static panorama demos.

Making my first A-Frame Component

Following Kevin Ngo’s advice, I decided to make a new component to allow me to modify either the landscape, ocean or mountain component’s material’s side property to allow them to be viewed from below. I started by creating a new file, material-side-modifier.js in my /src/js folder:

AFRAME.registerComponent('material-side-modifier', {
 //dependencies: ['yourlandscapecomponent'], // Or wait on an event.

init: function () {
 // Modify material side here.
 }
});

Then I modified my main.js to require my new .js file:

require('aframe');
require('aframe-terrain-model-component');
var extras = require('aframe-extras');
// Register everything.
extras.registerAll();
require('aframe-mountain-component');
require('kframe');
//requiring my first component! https://github.com/substack/browserify-handbook told me how to do this
require('./material-side-modifier.js');

Next I created a new .pug file, terrainModelComponentWithMyComponent.pug in my /src/pug folder, to use my new component:

doctype html
html
 head
 meta(charset='utf-8')
 title A-Frame Terrain Model Component with Panoramic background
 meta(name='description', content='A-Frame Terrain Model Component with Panoramic background')
 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)
 //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)
 //Sky
 a-sky(src='#sky')
 //Light
 a-entity(light='type: ambient;')

I edited material-side-modifier.js to be as follows:

AFRAME.registerComponent('material-side-modifier', {
 //dependencies: ['yourlandscapecomponent'], // or wait on an event.
 // TODO: add waiting for an event to this instead of dependencies.
 // Thanks to @pookage (http://www.beardeddevelopment.co.uk/) on the A-Frame Slack for pointing me in the correct direction:https://developer.mozilla.org/en/docs/Web/API/GlobalEventHandlers/onload
 //Thanks to @dietrich () for suggesting changing from window.onload = changeMaterialSide; to document.addEventListener(‘DOMContentLoaded’, changeMaterialSide);

// Allow material-side-modifier component default to 2
 schema: {
 type: 'int', default: 2 //just a single-property schema, parse and stringify should be inferred.
 },
 // Update the side property of the material of the component to 2 aka THREE.DoubleSide, see: https://threejs.org/docs/#Reference/Materials/Material
 init: function () {
 console.log("A-Frame and the rest of the components have loaded");
 var object3D = this.el.object3D;
 console.log("Object3D is ", object3D);
 var side = this.data; //should be 2, the default value
 console.log("The value of data (side) is ",side); //why isn't this reporting properly?

//via http://stackoverflow.com/questions/18613295/how-to-set-a-texture-on-a-object3d-child-mesh
 console.log("object3D has: ", object3D.children, " children.");
 for(var i in object3D.children) {
 //for all the children of the landscapeObject3D, change the material side property to THREE.DoubleSide aka 2
 console.log("(Method 1: The current number of sides is", object3D.children[i].material.side);
 object3D.children[i].material.side = THREE.DoubleSide;
 console.log("Method 1: The updated number of sides is", object3D.children[i].material.side);
 }

// via: http://stackoverflow.com/questions/16027131/three-js-how-to-make-double-sided-object
 object3D.traverse( function( node ) {
 console.log("Traversing...");
 if( node.material ) {
 console.log("Method 2: The current number of sides is",node.material.side);
 node.material.side = THREE.DoubleSide;
 node.material.needsUpdate = true;
 console.log("Method 2: The updated number of sides is",node.material.side);
 }
 });
}
});

Accessing the new webpage I just created resulted in the following output:

build.js:70503 A-Frame Version: 0.3.2
build.js:70504 three Version: ^0.76.1
build.js:70505 WebVR Polyfill Version: 0.9.15
build.js:88899 A-Frame and the rest of the components have loaded
build.js:88901 Object3D is THREE.Group
build.js:88903 The value of data (side) is NaN
build.js:88906 object3D has: Array[1] children.
build.js:88916 Traversing...
build.js:38629 THREE.WebGLRenderer 76

Which suggested to me that neither of my loops to change the material properties are functioning as desired. Should I be using update instead of init? Or should I be waiting for a completely different event? Or do I need to specify custom dependencies? I’d really rather not do the latter as I want this component to be able to change the side property of any component that has a material.

After chatting on the A-Frame Slack, I found and filed a nasty bug for a Single Property Schema, and changed my component to use the Multi Property Schema syntax. I also found a silly mistake in my gulpfile.js, which I corrected, so that it watched for all Javascript file changes, rather than just main.js.

I simplified my component to the following:

AFRAME.registerComponent('material-side-modifier', {
 // This component can be used multiple times
 multiple: true,
 // Allow material-side-modifier component a single property schema, of type int, defaulting to 2, aka THREE.DoubleSide, see https://threejs.org/docs/#Reference/Materials/Material.side
 schema: {
 side: {
 type:'int',
 default: 2
 }
 },
 update: function () {
 console.log("Inside of update() of material-side-modifier");
 var side = this.data.side; //should be 2, the default value, all I want to be able to do is material.side = side; - change the side property of the material to
 var object3D = this.el.object3D;

 console.log("Starting traverse of object3D");
 object3D.traverse( function( child ){
 console.log("Traversing...")
 console.log("The current child object is: ", child);
 console.log("The type of the child is", typeof child);
 if ( child instanceof THREE.Group ) {
 console.log("Found a THREE.Group!")
 child.traverse(function(childOfChild) {
 console.log("Traversing the traversing...")
 console.log("The current child of child object is: ", childOfChild);
 console.log("The type of the child is", typeof childOfChild); //how is the mesh object always one away from me?
 if ( childOfChild instanceof THREE.Mesh ) {
 console.log("Found a THREE.Mesh!")
 }
 }
 );
 }
 }
 );
 console.log("Finished traverse of object3D");
 }
});

Then I found this issue on the Three.js Github where the author of three.js (MrDoob) states:

Geometries and Materials are not children.

Which means that the traverse() method of Object3D in three.js doesn’t iterate through the Materials and Geometries of Object3D’s. So I’m never going to get to my material by the above method. What method should I use?

After further discussions on the A-Frame Slack on the #Learning channel, I altered my new component to be as follows:

AFRAME.registerComponent('material-side-modifier', {
 // This component can be used multiple times
 multiple: true,
 // Allow material-side-modifier component a single property schema, of type int, defaulting to 2, aka THREE.DoubleSide, see https://threejs.org/docs/#Reference/Materials/Material.side
 schema: {
 side: {
 type:'int',
 default: 2
 }
 },
 tick: function () {
 var side = this.data.side; //should be 2, the default value, all I want to be able to do is material.side = side; - change the side property of the material to
 var object3D = this.el.object3D;
 if(this.el.getObject3D('terrain')){
 var terrain = this.el.getObject3D('terrain');
 terrain.material.side = THREE.DoubleSide;
 }
 }
});

As can be seen on this demo page, the component finally works, albeit in a way that is custom to the component it alters, the Terrain Model component.

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.