Another Sky is a series of global scale augmented reality installations, inspired by Digital Life (i.e. the integration of digital and analogue worlds), Artificial Life and Weaving. Weaving is the beginning of modern computing – the Jacquard Loom was the first programmable object. Charles Babbage was inspired by it to attempt to build the Difference Engine and Analytical Engine. Weaving is also a Human Universal – whereas writing is not.
Each member of the Another Sky series builds on the system and technology of the previous:
Another Sky I – a global scale sculpture that encodes every longitude and latitude on earth as a colour.
Another Sky II – a sculpture that encodes every longitude and latitude as a colour and the height of a virtual surface.
Another Sky III – a sculpture that encodes every longitude and latitude as a colour, a height and a digital organism that can live and die.
Another Sky IV – a sculpture that shows a global ecosystem that migratory life that can be fed by humans.
After reading Javascript: The Good Parts by Douglas Crockford I decided to make some minor changes to the codebase of the suite:
Instead of using the postfix increment operator ++ use += 1
Instead of using the == comparison operator which uses type coercion , use the more reliable ===. Similarly for !=, use !==.
After making those changes, I screen grabbed each Reactickle to make an icon. Midway through the process I realised that it was difficult to tell the difference between some of the mouse based interactions and the keyboard interactions, so decided to make animated video icons instead of a still.
I used Screeny to be able to grab at a fixed pixel dimension of 256×256, but drawn at 128×128 in the main menu.
After adding the video thumbnails I spotted a bug with the KeyboardFountain – objects were being added to the matter.js simulation, but never removed.
First I made sure that the array holding all the circles was empty before I started adding to it:
Taking each Reactickle one by one, I started with KeyboardScalingCircleGrid.
On line 14 of KeyboardScalingCircleGrid.js, I had a console output, detailing the size of the array that held each of the ScalingCircles, one for each keyboard button I was taking input from. Repeatedly starting the Reactickle and going back to the main menu resulting in the following output:
KeyboardScalingCircleGrid.js:14 The size of the circles array is 216
KeyboardScalingCircleGrid.js:14 The size of the circles array is 252
KeyboardScalingCircleGrid.js:14 The size of the circles array is 288
I added this to every other Reactickle that had a similar array bug.
While testing every Reactickle to confirm that I’d expunged the array bug, I discovered that that both KeyboardSquares.js and KeyboardFountain.js used a centred rectMode() rather than a cornered one. I added a rectMode() change before each of their draw calls, as well as reseting to corner mode in the main sketch.js to ensure that the GUI was drawn correctly.
Next will be creating some icons for the Reactickles, rather than the current placeholder text.
After talking to Wendy, we agreed that the next step was to get all the individual sketches working in a suite.
I created a new folder to contain the suite: Reactickles3, and started by trying to run KeyboardScalingCircleGrid within a larger p5.js sketch.
I created a new sketch.js in the Reactickles3 folder and copied KeyboardScalingCircleGrid.js into the same folder. After making the entire KeyboardScalingCircleGrid a class of its own, I was able to call its setup, draw and keyTyped methods from within my main sketch.js.
With that functioning, I started to test other Reactickles in this new structure, starting with KeyboardWorm, followed by MouseWorm, KeyboardSpringyCircles, MouseSpringyCircles, KeyboardSquares and finally KeyboardFountain.
Once those were working, I created an array of those Reactickles as well as a main menu screen and a return to main menu button. There are still some bugs in the system, but I’m going to save those for the next session.
The original KeyboardSquares can be seen above – on key presses the larger square broke up into smaller squares and flew apart, as well as changing colour. It was a relatively quick port to create, based on previously ported Reactickles. One sticking point was making sure that the larger square was always centred – even if the user changed the size of the window of the browser. Luckily p5.js has a windowResized() function to detect that. I also found some different options for emptying an array in JavaScript. The ported version can be found here.
The next Reactickle was Keyboard Fountain which can be seen from 01:43- 02:20 on the video above.
After creating KeyboardFountain in my local Git repository, I set about creating the “fountain” at the bottom of the screen. Conveniently, p5.js has an existing triangle drawing function, called triangle(). Quoting from the reference:
Description
A triangle is a plane created by connecting three points. The first two arguments specify the first point, the middle two arguments specify the second point, and the last two arguments specify the third point.
Syntax
triangle(x1,y1,x2,y2,x3,y3)
I created the following function to draw my Fountain:
function drawFountain(){
var fountainWidth = 50; //50 pixels wide
var fountainHeight = 50; //50 pixels high
var translatedX = this.fountainPosition.x * windowWidth;
var translatedY = this.fountainPosition.y * windowHeight;
var redColour = color(0,100,100,100);
fill(redColour);
triangle(translatedX-(fountainWidth/2),translatedY,translatedX+(fountainWidth/2),translatedY,translatedX,translatedY-fountainHeight); //https://p5js.org/reference/#/p5/triangle
}
After I was happy with the drawing of my Fountain, I added interactivity by using the p5.js keyCode variable to allow the user to use the left and right arrows keys to move the fountain:
function keyPressed(){
var moveAmount = 0.01;
if (keyCode == LEFT_ARROW) { //https://p5js.org/reference/#/p5/keyCode
if(this.fountainPosition.x >= moveAmount){
this.fountainPosition.x -= moveAmount;
}
} else if (keyCode == RIGHT_ARROW) {
if(this.fountainPosition.x <= (1-moveAmount)){
this.fountainPosition.x += moveAmount;
}
}
return false; // prevent default
}
However, this meant that the user had to repeatedly press the arrow keys to move, whereas I wanted the Fountain to keep moving as long as I held the key down. I found the keyIsDown function which allowed me to have the Fountain move in the way that I wanted:
function moveFountain(){
var moveAmount = 0.005;
if (keyIsDown(LEFT_ARROW) && (this.fountainPosition.x >= moveAmount))
this.fountainPosition.x -= moveAmount;
if (keyIsDown(RIGHT_ARROW) && (this.fountainPosition.x <= (1-moveAmount)))
this.fountainPosition.x += moveAmount;
}
In order to get the particles flying out of the fountain in a realistic way, I new I would have to create some kind of 2D physics simulation – at the very least using basic projectile physics. In the spirit of keeping it simple, I looked online to see if there were any existing physics libraries that worked with p5.js. Thanks to the fantastic Coding Train by Daniel Shiffman, I discovered matter.js.
I initialised matter.js by duplicating most of Daniel’s code in a new function called setupPhysics, after declaring the necessary variables for matter.js to run:
//matter-js and p5.js integration based on https://github.com/shiffman/p5-matter by Daniel Shiffman
//also see https://www.youtube.com/watch?v=urR596FsU68 introduction to matter.js by Daniel Shiffman
var Engine = Matter.Engine;
//var Render = Matter.Render; // commented out as we are using p5.js to render everything to the screen
var World = Matter.World;
var Bodies = Matter.Bodies;
var Body = Matter.Body;
var Composite = Matter.Composite;
var Composites = Matter.Composites;
var engine;
var world;
var bodies;
var canvas;
function setupPhysics(){
// create an engine
engine = Engine.create();
world = engine.world;
//make walls to constrain everything
var params = {
isStatic: true
}
var ground = Bodies.rectangle(width / 2, height, width, 1, params);
var leftWall = Bodies.rectangle(0, height / 2, 1, height, params);
var rightWall = Bodies.rectangle(width, height / 2, 1, height, params);
var top = Bodies.rectangle(width / 2, 0, width, 1, params);
World.add(world, ground);
World.add(world, leftWall);
World.add(world, rightWall);
World.add(world, top);
// run the engine
Engine.run(engine);
}
I changed keyTyped to trigger a new function, addCircle – as well as adding code to draw all the new circles that would be created:
function addCircle(){
var params = {
restitution: 0.7,
friction: 0.2
}
var translatedX = this.fountainPosition.x * windowWidth;
var translatedY = this.fountainPosition.y * windowHeight;
var radius = 21;
var newCircle = Bodies.circle(translatedX, translatedY-(fountainHeight), radius, params);
circles.push(newCircle);
World.add(world, newCircle);
//set a random velocity of the new circle
//see http://brm.io/matter-js/docs/classes/Body.html
//from http://codepen.io/lilgreenland/pen/jrMvaB?editors=0010#0
Body.setVelocity(newCircle, {
x: random(-5,5),
y: -random(15,30)
});
}
function drawCircles(){
stroke(255);
strokeWeight(1);
fill(randomColour);
for (var i = 0; i < circles.length; i++) {
var circle = circles[i];
var pos = circle.position;
var r = circle.circleRadius;
var angle = circle.angle;
push();
translate(pos.x, pos.y);
rotate(angle);
ellipse(0, 0, r * 2);
line(0, 0, r, 0);
pop();
}
}
function keyTyped(){
addCircle();
return false; //https://p5js.org/reference/#/p5/keyTyped preventing default behaviour
}
Every Where allows anyone to add augmented reality content to their environment.
Example use cases:
Museums:
After digitising their collection, museums could use Every Where to display it in the local area surrounding their building – or place it at any location in the entire world. For example, the British Museum could display the Rosetta Stone at a huge scale floating above Bloomsbury in London, in it’s original location in Memphis, Egypt or above a primary school in Leeds that recently visited the museum on a school trip.
Libraries: Libraries could use Every Where to display information or content at an architectural scale over their locality. Imagine a quote from the book of the week looming over the town square or a drawing from the pre-school playgroup floating above the library entrance.
Local History: Residents could use Every Where to add photography back to the place where it was taken. A grandfather could add a photo of him and his brother celebrating England’s football world cup win of 1966 in the street where he grew up at the exact point it was taken – creating a historical Trompe-l’œil.
Digital Graffiti: Local children could use Every Where to write in letters ten metres high or add 3D emoticons to their street as well as learning how to program in augmented reality along the way.
Unlimited Sculpture:
Artists could use Every Where to add sculpture to their world unencumbered by budget or physical possibility.
Hardware:
Every Where can either run on a traditional web server, in the cloud or locally on a Raspberry Pi.
Every Where runs locally by creating a local WiFi network on the Raspberry Pi itself. In this way Every Where can function even in places that are out of the range of cellular data services or broadband. Additionally, Every Where can be solar powered, allowing it to run “Off Grid” – completely independently of all traditional public utility services at a very low cost. It will also be possible to create a mesh of local Every Where units to create an independent network for hosting a variety of content.
Software:
Every Where runs atop A-Frame and Node.js. A-Frame allows compatibility with a wide variety of hardware platforms – Vive, Rift, desktop and mobile platforms.
Every Where not only allows for viewing of content but the creation of content via a built in editor.
However, I am still developing the project and concept and would be very interested in repurposing it for another country, or even on a global scale. Get in touch if you’d like to help make it happen! Huge thanks to the British Council for supporting me to build the proof of concept.
I found this explanation of Robert Penner’s easing equations very useful – the most useful being the idea that one is feeding a timing value between 0 and 1 to the easing function to get back another value how far along the transformation in question you are – and that this value may be greater than 1 or less than zero. This is exactly what I want in my springy circles – for them to overshoot their target and oscillate a few times before settling at the final position.
Starting with KeyboardSpringyCircles, I started adding the logic to allow for non-linear tweens for movement. I got timing working by using the the millis() function of p5.js.
function SpringyCircle(){ //SpringyCircle object
this.colour = color(random(100),50,100,50);; //random hue, saturation 50%, brightness 100%, alpha 50%
this.radius = random(circleMinRadius,circleMaxRadius);
this.position = createVector(random(windowWidth)/windowWidth,random(windowHeight)/windowHeight);
this.startPosition = createVector(this.position.x, this.position.y);
this.startPosition.y += 0.15; //want to start 15% of the screen down when the circle is interacted with
this.durationOfTween = 1000; //1000 milliseconds for tween
this.endPosition = createVector(this.position.x, this.position.y); //want to finish back where we started
this.startTimeOfTween = -1;
this.display = function(){
var milliseconds = millis();
var elapsedMillisSinceStartOfTween = milliseconds - this.startTimeOfTween;
if(this.startTimeOfTween > 0 && elapsedMillisSinceStartOfTween < this.durationOfTween){
var changeBetweenStartAndEnd = this.endPosition.y - this.startPosition.y;
var ratioOfTweenComplete = elapsedMillisSinceStartOfTween/this.durationOfTween;
var changeUpToNow = changeBetweenStartAndEnd*this.bouncePast(ratioOfTweenComplete);
this.position.y = this.startPosition.y + changeUpToNow;
}
var translatedX = this.position.x * windowWidth;
var translatedY = this.position.y * windowHeight;
fill(this.colour);
ellipse(translatedX, translatedY, this.radius); // https://p5js.org/reference/#/p5/ellipse and https://p5js.org/reference/#/p5/ellipseMode
}
this.startTween = function(){ //move the position of the spring a bit
print("Starting a tween");
this.startTimeOfTween = millis();
}
this.bouncePast = function(howFarThroughTween){
//see https://github.com/jeremyckahn/shifty/blob/master/src/shifty.formulas.js
//and http://upshots.org/actionscript/jsas-understanding-easing
//and of course http://robertpenner.com/easing/
if (howFarThroughTween < (1 / 2.75)) {
return (7.5625 * howFarThroughTween * howFarThroughTween);
} else if (howFarThroughTween < (2 / 2.75)) {
return 2 - (7.5625 * (howFarThroughTween -= (1.5 / 2.75)) * howFarThroughTween + 0.75);
} else if (howFarThroughTween < (2.5 / 2.75)) {
return 2 - (7.5625 * (howFarThroughTween -= (2.25 / 2.75)) * howFarThroughTween + 0.9375);
} else {
return 2 - (7.5625 * (howFarThroughTween -= (2.625 / 2.75)) * howFarThroughTween + 0.984375);
}
}
}
One bug that caused me intense frustration was that my local webserver didn’t seem to be serving the latest code when I made an update. Eventually I tracked it down to Chrome caching files wherever possible – meaning that I had to use Command Shift R to force a reload of all the files being served – not just the HTML.
I also managed to repeated Vector copying values explicitly bug from late November, by doing writing:
At this stage I had the following interaction working:
Which was a good start, but I didn’t like the precise settling animation, so I decided to create a new external JS file with all the easing equations contained in one convenient place. This would allow me to use all the easing equations in other places in my code.
I created easing.p5.jgl.js and keyboard.p5.jgl.js to contain all my easing logic and keyboard logic respectively. After adding both references to my index.html file:
I began going through all the possible options for easing:
var ratioOfEaseComplete = elapsedMillisSinceStartOfEase/this.durationOfEase;
// exhaustively trying all the different easing possibilities
// var changeUpToNow = changeBetweenStartAndEnd*easeOutQuad(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInOutQuad(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInCubic(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeOutCubic(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInOutCubic(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInQuart(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeOutQuart(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInOutQuart(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInQuint(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeOutQuint(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInOutQuint(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInSine(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeOutSine(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInOutSine(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInExpo(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeOutExpo(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInOutExpo(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInCirc(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeOutCirc(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInOutCirc(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeOutBounce(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInBack(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeOutBack(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeInOutBack(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*elastic(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*swingFromTo(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*swingFrom(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*swingTo(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*bounce(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*bouncePast(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeFromTo(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeFrom(ratioOfEaseComplete);
// var changeUpToNow = changeBetweenStartAndEnd*easeTo(ratioOfEaseComplete);
I broke this down to the following options, which felt like they were in the correct ballpark:
// var changeUpToNow = changeBetweenStartAndEnd*easeOutBounce(ratioOfEaseComplete);
// easeOutBounce is good but not right
// var changeUpToNow = changeBetweenStartAndEnd*easeOutBack(ratioOfEaseComplete);
// easeOutBack is also good but not right
// var changeUpToNow = changeBetweenStartAndEnd*elastic(ratioOfEaseComplete);
// elastic is good
// var changeUpToNow = changeBetweenStartAndEnd*swingTo(ratioOfEaseComplete);
// swingTo is good
// var changeUpToNow = changeBetweenStartAndEnd*bounce(ratioOfEaseComplete);
// bounce is good
// var changeUpToNow = changeBetweenStartAndEnd*bouncePast(ratioOfEaseComplete);
// bouncePast is good, but feels abrupt at end
It was proving laborious to manually change the code every time I wanted to see how the particular easing function looked. Luckily, p5.js comes with a library called p5.dom:
The web is much more than just canvas and p5.dom makes it easy to interact with other HTML5 objects, including text, hyperlink, image, input, video, audio, and webcam.
p5.dom was even already included, but commented out in my index.html file, so I just removed the comment:
<script language="javascript" type="text/javascript" src="../libraries/p5.js"></script>
<!-- uncomment lines below to include extra p5 libraries -->
<script language="javascript" src="../libraries/p5.dom.js"></script>
<!--<script language="javascript" src="../libraries/p5.sound.js"></script>-->
<script language="javascript" src="../libraries/keyboard.p5.jgl.js"></script>
<script language="javascript" src="../libraries/easing.p5.jgl.js"></script>
<script language="javascript" type="text/javascript" src="sketch.js"></script>
Following that I added some logic to link the select object to the selection of the particular easing function that I wanted, using the JavaScript switch statement:
var changeUpToNow = 0;
var easeOption = sel.value();
switch(easeOption){
case 'easeOutBounce':
changeUpToNow = changeBetweenStartAndEnd*easeOutBounce(ratioOfEaseComplete);
break;
case 'easeOutBack'):
changeUpToNow = changeBetweenStartAndEnd*easeOutBack(ratioOfEaseComplete);
break;
case 'elastic'):
changeUpToNow = changeBetweenStartAndEnd*elastic(ratioOfEaseComplete);
break;
case 'swingTo'):
changeUpToNow = changeBetweenStartAndEnd*swingTo(ratioOfEaseComplete);
break;
case 'bounce'):
changeUpToNow = changeBetweenStartAndEnd*bounce(ratioOfEaseComplete);
break;
case 'bouncePast'):
changeUpToNow = changeBetweenStartAndEnd*bouncePast(ratioOfEaseComplete);
break;
default:
changeUpToNow = changeBetweenStartAndEnd*bouncePast(ratioOfEaseComplete);
break;
}
I decided that “elastic” was the closest to the original version, but still wasn’t satisfied with the result. I found this spring example on the Processing.js site and decided to port it to p5.js. After realising that I had to rewrite the code to use the static methods of the p5.vector class, I got to something that was much closer to the original version. I increase the number of circles to 100 as an added bonus. Give the updated KeyboardSpringyCircles demo a try.
It’s true! By page 11 I’d realised what had been causing the bug that had been bothering me yesterday. My code for clicking to select which circle should bounce was incorrect:
if(distanceBetweenMouseAndCircle < (springyCircles[i].radius)){ //this is resulting in a bug - radius/2 works properly, but why isn't radius alone giving the correct interaction?
//if the mouse is under the springy circle, then spring/move it
springyCircles[i].moveSpring();
}
This fixed all my circle selection code. The next step is to convert all my radius variables to be proportional to the screen size (as my position co-ordinates already are) rather than being pixel based. It’s important to me that Reactickles 3 is resolution independent – i.e. that it looks the same on a variety of different screen sizes and proportions.