Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase Javascript + P5.js: Asynchronous function getting in the way of redrawing the canvas

I'm trying to create an application where circles are drawn onto the canvas through reading information from a Firebase database that stores the x and y coordinates of the circles. Executing the code below however, simply produces nothing, without any sign of the circles, because the function drawCricles runs asynchronously, and thus the command background(40) clears everything before the circles can be drawn.

Here is my code:

function setup() {
    createCanvas(windowWidth, windowHeight); 
    background(40); 
    stroke(80); 
    smooth();
    frameRate(60);
}

function drawCircles() {
    firebase.database().ref("circles").once("value", function(snapshot) {
        var snapshotVal = snapshot.val();
        var circleCount = snapshotVal.numCircles;

        for (var j = 0; j < circleCount; j++) {
            firebase.database().ref("circles" + j).once("value", function(snapshot) {
                var snapshotValue = snapshot.val();
                fill(143, 2, 2);
                ellipse(snapshotValue.xPos, 50, 50);
            });
        }
    });
}

function draw() {
    stroke(80);
    background(40);

    stroke(0);
    drawCircles(); 
}
like image 606
Adam Lee Avatar asked Nov 04 '17 03:11

Adam Lee


1 Answers

It seems your problem is simply 60 frames per second, which is causing a foot-race condition. Firebase .once will execute async when it completes fetching, and P5 won't wait for it to fetch because it'll stick with its framerate timing.

In this specific case I have multiple recommendations which will hopefully get you very close to the result you desire.

1 - Restructure your code

There's two issues with the current structure of your code.

  • Case 1 : Your current code would lead me to think your circles are updated in realtime in the database, and you need to stay up to date, so you keep fetching their latest position. If this is the case, you should use .on("value") instead of .once("value") and let firebase send you the updates whenever circles change, instead of asking it 60 times a second to save roundtrip request time. If this is the case : See my solution 1 below.

  • Case 2 : If your circles aren't updated real-time in the database, and you just want the whole list of circles, then you're fetching the list 60 times a second for no reason. You should instead fetch the list using .once upon setup and iterate through that list in draw() later. See solution 2 below.

2 - Restructure your database

In either case, your current database model requires you to keep fetching in a loop. Which means you're making as many requests as your circleCount. This is bad for your usage, simply because each request takes additional trip time, and we're trying to reduce the time it takes, so that it would be closer to real-time. (or match the framerate)

Currently your circles are seemingly saved as circles1 circles2 etc all at root, because you're using .ref("circles" + j) to retrieve them. Make it so that you save your circles like this : .ref("circles/" + j) that would mean that each circle is now saved in circles. like circles/circle1 circles/circle2 etc.

The benefit of this is that now you don't need the additional requests to firebase get all circles. Firebase has incredibly convenient things like forEach to iterate through all children with a single request.

3 - Clear background in your firebase callback

Currently, you clear the background in a frame-rate specific manner. This means that if each of your firebase calls take longer than 1/60th of a second (16 miliseconds) you will have cleared the background and move on. Chances of you achieving this speed is very low even after we structure our database. so instead, I would recommend first using 30fps, which would also reduce the number of calls you will make to firebase to 30 calls per second.

Solution 1

If your circles are updated in the database (say for example by some other game-player or someone else, and you want your code to always display the latest xPos)

var latestCirclePositionsSnapshot;

function setup() {
  createCanvas(windowWidth, windowHeight); 
  background(40); 
  stroke(80); 
  smooth();
  frameRate(60);

  firebase.database().ref("circles").on("value", function(snapshot) {
    // got a new value from database, so let's save this in a global variable. 
    latestCirclePositionsSnapshot = snapshot;
    // we will keep drawing this update until we get a new one from the database.
  });
}

function draw() {
  drawCircles(); 
}

function clearBackground () {
  stroke(80);
  background(40);
}

function drawCircles() {
  clearBackground();
  stroke(0);  
  latestCirclePositionsSnapshot.forEach(function(circleSnapshot) {  
    // circleData will be the actual contents of each circle
    var circleData = circleSnapshot.val();
    fill(143, 2, 2);
    ellipse(circleData.xPos, 50, 50);
  });
}

Basically this will keep drawing the last circle positions we got from firebase until we get a new one. (so P5 will keep refreshing at 60fps, but your firebase updates will be as realtime as firebase can run and fetch from firebase etc.)

Solution 2

If you don't have real-time updates in your database, and all you'd like is to draw circles by getting data from firebase once (say for example to plot some dots based on some data)

var circlePositions;
var gotPositions = false;

function setup() {
  createCanvas(windowWidth, windowHeight); 
  background(40); 
  stroke(80); 
  smooth();
  frameRate(60);

  firebase.database().ref("circles").once("value", function(snapshot) {
    // got the circle values from the database
    // let's store them and we'll keep drawing them forever. 
    circlePositions = snapshot;
    gotPositions = true;
  });
}

function draw() {
  drawCircles(); 
}

function clearBackground () {
  stroke(80);
  background(40);
}

function drawCircles() {
  clearBackground();
  stroke(0); 

  if (gotPositions) {
    circlePositions.forEach(function(circleSnapshot) {  
      // circleData will be the actual contents of each circle
      var circleData = circleSnapshot.val();
      fill(143, 2, 2);
      ellipse(circleData.xPos, 50, 50);
    });
  } else {
    // Display some text here like "LOADING DATA FROM SERVERS..." 
  }
}

Hope these help :) It's good to see another fellow fan of Processing & Firebase.

like image 104
johnozbay Avatar answered Oct 09 '22 14:10

johnozbay