Pointillism with Pubnub

Authored by John Kuiphoff


In this quick tutorial, we will create a realtime, multiplayer Pointillism canvas drawing tool. To do this, we are going to use p5js and Pubnub. Pubnub enables developers to build realtime apps for connected devices. You can create chat rooms, multiplayer games, sensor-based networks, etc. I've been working with Pubnub for the last few months and the possibilities are endless.

Open the demo below using two separate browsers (ex: Chrome and Firefox). Resize each browser window so that you can see each browser side by side. Then, start making dots.

View Demo

Hello Pubnub

Pubnub is an incredibly powerful tool that makes it very easy to create multiuser applications using only a few lines of code. Before we begin, you will need to sign up for an account on Pubnub's website: http://pubnub.com. The video below from Pubnub will show you how:

Got your keys? Good. We'll need them a little later.

First, let's start off by including the Pubnub library into our HTML page. Depending on whether you are using the P5js editor or SublimeText, your code might look slightly different. The important thing is that you include the Pubnub library:

<!DOCTYPE html>
<html>
  <head>
    <title>Pointillism</title>
    <script language="javascript" src="../../p5/p5.js"></script>
    <script language="javascript" src="../../p5/addons/p5.dom.js"></script>

    <!-- Pubnub -->
    <script src="http://cdn.pubnub.com/pubnub.min.js"></script>
  
    <script language="javascript" src="sketch.js"></script>
    <style> body {padding: 0; margin: 0;} canvas {border: 1px solid #ccc; } </style> 
  </head>
  <body>
  </body>
</html>

Single Player

Next, we will create a simple sketch using p5js that draws a circle on the screen when a user clicks the mouse. There is nothing really new here - it's a lot like the drawing tool we made at the beginning of the semester. The only newish thing is that I'm using the colorMode(HSB) (which stands for Hue, Saturation, Brightness). This allows me to get a lot of different colors using a single slider.

var pubnub;
var slider;

function setup() 
{
  // canvas size
  createCanvas(600, 600);

  // HSB: Hue Saturation Brightness
  colorMode(HSB, 255);
  
  // slider has a range between 0 and 255 with a starting value of 127
  slider = createSlider(0, 255, 127);
  slider.position(60, 25);
}

function draw() 
{
   // draw a color swatch
   noStroke();
   fill(slider.value(), 255, 255);
   rect(25, 25, 25, 25);
}

function mousePressed()
{
  if(mouseY > 100)
  {
    // draw what the user is drawing immediately
    noStroke();
    fill(slider.value(), 255, 255);
    ellipse(mouseX, mouseY, 20, 20);
  }
}

You might want to check to make sure that your simple sketch is working. At this time, it is only "single player"

Multiplayer

Now that we a working single player drawing tool - let's make it a collaborative drawing tool so that anyone visiting the page draw with you in real time.

First, declare two new variables at the top of your sketch:

var uniqueid;
var pubnub;

Then initialize Pubnub using the keys we received when we signed up. Once you have your subscribe and publish keys, type in the follow code into your sketch's setup function:

// create a unique_id
uniqueid = PUBNUB.uuid();

// initialize pubnub
pubnub = PUBNUB.init(
{
  publish_key   : 'YOURPUBLISHKEY',
  subscribe_key : 'YOURSUBSCRIBEKEY',
  uuid: uniqueid
});

The uniqueid variable is a random string of characters that gets assigned to the person visiting the page. This will allow us to keep track of who is doing what later in the sketch.

Now that we're plugged in, let's subscribe to a channel. It might be important to note that you can subscribe to a channel - meaning that you can listen to messages being broadcasted to it. You can also publish a message to a channel - meaning that you can contribute stuff to it that gets broadcasted to everyone else subscribed to the channel. That's why you have two separate keys - one allows you to listen - the other allows you to contribute. So, let's subscribe to a channel called "drawing" - add the following code to your sketch's setup function (after the pubnub init):

// subscribe to drawing channel
pubnub.subscribe(
{
  channel : "drawing",
  message: handleMessage
});

This code subscribes us to the drawing channel, but what's this thing with handleMessage? Basically, when a message is broadcasted to this channel, it will be passed to a function named "handleMessage" (although, we could have named it something else). Let's create that function now. Add the following code to your sketch (at the bottom is fine):

// when we receive a message from pubnub
function handleMessage(message) 
{
  // draw a circle on the screen if the user is someone else
  if(message.uniqueid != uniqueid)
  {
    noStroke();
    fill(message.c, 255, 255);
    ellipse(message.x, message.y, 20, 20);
  }
}

The "handleMessage" function gets the "message" as a parameter. The message is actually more than text - it is a JSON object that contains an x position, y position, color and a uniqueid. More on this a little later.

This function will first check to see if the message is coming from someone else (no, not you). If the message is coming from someone else - draw an ellipse with the passed color, x position and y position. Why are we only drawing the shapes of strangers you ask? Well, even though Pubnub works in real time, there is a tiny bit of lag. After all, we are shuttling data all over the world as fast as we can to make this work. But still, there's a tiny bit of lag. So, this function only draws something if it is coming from someone else. In the next bit of code, we are going to write some code that checks if YOU are drawing, the drawing gets drawn immediately to the screen (so that there is no lag) and then some data gets published to all of the other people subscribed to the channel.

Next, add the following drawing and publishing code to your sketch:

function mousePressed()
{
  if(mouseY > 100)
  {
    // publish drawing data
    pubnub.publish(
    {
      channel: 'drawing',
      message: {
        x: mouseX,
        y: mouseY,
        c: slider.value(),
        uniqueid: uniqueid
      }
    });

    // draw what the user is drawing immediately
    noStroke();
    fill(slider.value(), 255, 255);
    ellipse(mouseX, mouseY, 20, 20);
  }
}

When the mouse is pressed, we first check to make sure that that the mouseY is more than 100 pixels down. Why? Because we don't want to draw over our slider. So, if mouseY is more than 100: Publish our data to the 'drawing' channel so that anyone subscribed to it will receive our data. We are passing our x position, y position, color and our uniqueid. Finally, draw a circle on the screen (without lag).

Hi Linda and Allyee! Upload your example to a web server using Fetch or Cyberduck and check to see if things are working. Now, try to add some additional options like brush size, different shapes, etc.

A few more notes

In this example, I chose to make individual circles with each mousePress instead of streaming a ton of circles (or lines) continuously. If each user published 60 messages per second, you would start to notice some lag. You could certainly change this example to make it work - just be creative in the how you approach it. Perhaps, limit the number of times you publish per second. Or, create more interesting drawings that use lines and nodes together - it's a simpler drawing but still really neat.

Full code for sketch.js

var pubnub;
var uniqueid;
var slider;

function setup() 
{
  // canvas size
  createCanvas(600, 600);

  // HSB: Hue Saturation Brightness
  colorMode(HSB, 255);
  
  // slider has a range between 0 and 255 with a starting value of 127
  slider = createSlider(0, 255, 127);
  slider.position(60, 25);

  // create a unique_id
  uniqueid = PUBNUB.uuid();

  // initialize pubnub
  pubnub = PUBNUB.init(
  {
    publish_key   : 'YOURPUBLISHKEY',
    subscribe_key : 'YOURSUBSCRIBEKEY',
    uuid: uniqueid
  });

  // subscribe to drawing channel
  pubnub.subscribe(
  {
    channel : "drawing",
    message: handleMessage
  });
}

function draw() 
{
   // draw a color swatch
   noStroke();
   fill(slider.value(), 255, 255);
   rect(25, 25, 25, 25);
}

function mousePressed()
{
  if(mouseY > 100)
  {
    // publish drawing data
    pubnub.publish(
    {
      channel: 'drawing',
      message: {
        x: mouseX,
        y: mouseY,
        c: slider.value(),
        uniqueid: uniqueid
      }
    });

    // draw what the user is drawing immediately
    noStroke();
    fill(slider.value(), 255, 255);
    ellipse(mouseX, mouseY, 20, 20);
  }
}

// when we receive a message from pubnub
function handleMessage(message) 
{
  // draw a circle on the screen if the user is someone else
  if(message.uniqueid != uniqueid)
  {
    noStroke();
    fill(message.c, 255, 255);
    ellipse(message.x, message.y, 20, 20);
  }
}
comments powered by Disqus