Build a Pictionary Web App with Twilio Video, Twilio Sync, and JavaScript

Developer working on a Pictionary app at his work station
January 27, 2023
Written by
Carlos Mucuho
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Introduction

In this tutorial, you will learn how to build a web app that allows you to play Pictionary in a video room. Pictionary is a game where players draw pictures on a board to help their team guess a word or phrase before the time runs out. Players take turns drawing and guessing.

To build this web app, you will use the Twilio Programmable Video JavaScript SDK to create a video room where room participants ( players ) can join and communicate with each other. You will use the Canvas API to build a whiteboard where the players can draw pictures. You will also use the Twilio Sync Javascript SDK to share the drawing on the whiteboard with all the players.

The Twilio Programmable Video JavaScript SDK is a collection of tools that allows you to add real-time video and voice capabilities to your web or mobile apps.

The Canvas API is a tool for creating graphics using JavaScript and HTML, it can be used for various purposes such as animation, game graphics, and real-time video processing.

Twilio Sync Javascript SDK is a software development kit that allows you to add real-time data synchronization functionality to your web and mobile applications.

By the end of this tutorial, you will have a web app that looks like the following:

Demo of the Pictionary web app built with Twilio Video and Sync APIs

Tutorial Requirements

To follow this tutorial you will need the following:

  • A free Twilio account
  • A basic understanding of how to use Twilio and Javascript to build a video web app;
  • Node.js v12+, NPM, and Git installed;

Getting the boilerplate code

In this section, you will clone a repository containing the boilerplate code needed to build the Pictionary web application.


This boilerplate code is a modified version of the code obtained from this tutorial.

Open a terminal window and navigate to a suitable location for your project. Run the following commands to create the project directory and navigate into it:

mkdir twilio-pictionary
cd twilio-pictionary

Use the following commands to clone the repository containing the boilerplate code and navigate to the boilerplate directory:

git clone https://github.com/CSFM93/twilio-pictionary-boilerplate.git
cd twilio-pictionary-boilerplate

This boilerplate code includes an Express.js project that serves the client application and generates the necessary access tokens for utilizing the Twilio Video and Sync APIs within the client application.

This Node.js application comes with the following packages:

  • dotenv: is a package that allows you to load environment variables from a .env file without additional dependencies.
  • express: is a lightweight Node.js web application framework that offers a wide range of features for creating web and mobile apps. You will use the package to create the application server.
  • nodemon: is a Node.js development utility that automatically restarts your application when a file is changed.
  • twilio: is a package that allows you to interact with the Twilio API.
  • uuid: is a package that is utilized to generate universally unique Identifiers, it will be used to create a unique identity when creating a Twilio access token.

Use the following command to install the packages mentioned above:

npm install

Collecting and storing your Twilio credentials

The .env file included in the cloned repository is where you will store your Twilio account credentials that will be needed to create access tokens for both the Twilio Video and Twilio Sync APIs. In total, you will need to collect and store four twilio credentials.

The first credential required is the Account SID, which is located in the Twilio Console. This credential should be stored in the .env file once obtained:

TWILIO_ACCOUNT_SID=<Your Account SID>

The second and third required credentials are an API Key SID and API Key Secret, which can be obtained by following this guide. After obtaining both credentials, copy and store them in the .env file:

TWILIO_ACCOUNT_SID=<Your Account SID>
TWILIO_API_KEY_SID=<Your API Key>
TWILIO_API_KEY_SECRET=<Your Secret Key>

After generating an API key you will only be able to see the API Key Secret one time. Be sure to copy and paste it somewhere safe, or directly into your .env file right away.

The fourth credential required is a Sync Service SID. A Sync Service is the top-level scope of all other Sync resources (Maps, Lists, Documents).

Navigate to the Twilio Sync Service page, create a new Sync Service, and copy the Sync Service SID. This credential should also be stored in the .env file once obtained:

TWILIO_ACCOUNT_SID=<Your Account SID>
TWILIO_API_KEY_SID=<Your API Key>
TWILIO_API_KEY_SECRET=<Your Secret Key>
TWILIO_SYNC_SERVICE_SID=<Your Sync Service SID>

Understanding the server.js file

Inside the server.js file, the express.js framework was used to create a web server that listens on port 3000, and the twilio module was used to interact with the Twilio API. The server has one endpoint named /join-room that accepts a POST request with a roomName property in the request body and returns a Twilio access token in the response body to the client.

The server uses the twilio module to either fetch or create the specified room by calling the findOrCreateRoom() function. It then generates an access token by calling the getAccessToken() function. The token is granted with a Video Grant and a Sync Grant, which allows the client to access the specified room and Twilio Sync service.


Please visit this page for more information on how to create access tokens for Twilio Video and Twilio sync. Also note that this tutorial, unlike the one where this code was extracted, utilizes group rooms, which are rooms that allow for a maximum of fifty participants at any given time. Please notice that these rooms aren’t free.

Understanding the public directory

This directory contains two files, index.html, and styles.css, that handle the client-side layout for a Pictionary application. The index.html file creates the user interface for the application, including a navigation bar, a canvas for drawing, and a div for displaying the webcam.

The file also links to the styles.css stylesheet and includes scripts for the Twilio Video and Twilio Sync SDKs, Bootstrap, a file named index.js, and a file named whiteboard.js. The styles.css file and Bootstrap are used to style the application. The index.js file will contain the code that creates a video room, while the whiteboard.js file will contain the code that creates a shareable whiteboard for drawing Pictionary words.

To see what this all looks like, go back to your terminal and use the following command to start your application server:

npm start

Then, in your browser, navigate to http://localhost:3000/.

Joining a video room

In this section, you will create the index.js file that was referenced in the boilerplate index.html file. This file is responsible for allowing the client application to request a Twilio access token with both a video grant and a Sync grant, use this access token alongside the Twilio Video SDK to join a video room, and display the players’ webcam feeds.

Open a new terminal tab and navigate to the public directory inside your project:

cd public

Create a file named index.js inside the public directory and add the following code to it:

const webcamFeedContainer = document.getElementById('webcam-feed-container');
let accessToken;

const startRoom = async () => {
  const roomName = 'myRoom';
  const response = await fetch('/join-room', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ roomName }),
  });
  const { token } = await response.json();
  accessToken = token;

  const room = await joinVideoRoom(roomName, token);
};

Here, first, the code gets an element from the DOM with the id of webcam-feed-container and declares a variable named accessToken. The element will be used to display the players’ webcam feeds, and the variable will be used to store and share the access token across files.

After getting the element that will be used to display the webcam feeds, a function named startRoom() was created.

Inside the startRoom() function, the room name was set and a fetch request was made to the '/join-room' endpoint to get an access token, then the token and the room name were passed to a function named joinVideoRoom().

Add the following code below the startRoom() function:

const joinVideoRoom = async (roomName, token) => {
  try {
    const room = await Twilio.Video.connect(token, {
      room: roomName,
    });
    return room;
  } catch (error) {
    console.log('error', error);
  }
};

The joinVideoRoom() function created above uses the Twilio Video SDK connect() method to join a room with the provided token and room name.

Add the highlighted code inside the startRoom() function, before the closing curly brace:

const startRoom = async () => {
  …

  const room = await joinVideoRoom(roomName, token);

  handleConnectedParticipant(room.localParticipant);
  room.participants.forEach(handleConnectedParticipant);
  room.on('participantConnected', handleConnectedParticipant);

  room.on('participantDisconnected', handleDisconnectedParticipant);
  window.addEventListener('pagehide', () => room.disconnect());
  window.addEventListener('beforeunload', () => room.disconnect());
};

Once the room is joined, the above code sets up event listeners for when new local or remote participants connect or disconnect.

When a participant connects, a function named  handleConnectedParticipant()—which you’ll create shortly—will be called.

When a participant disconnects, a function named  handleDisconnectedParticipant() will be called. You’ll create this shortly as well.

The code also set up event listeners for pagehide and beforeunload to disconnect the room when the user leaves the page.

Lastly, the code returns the access token.

Now, add the new handleConnectedParticipant() function below the joinVideoRoom() function:

const handleConnectedParticipant = async (participant) => {
  const participantDiv = document.createElement('div');
  participantDiv.setAttribute('class', 'participantDiv mt-2');
  participantDiv.setAttribute('id', participant.identity);
  webcamFeedContainer.appendChild(participantDiv);

  participant.tracks.forEach((trackPublication) => {
    handleTrackPublication(trackPublication, participant);
  });

  participant.on('trackPublished', handleTrackPublication);
};

When a new participant joins the video room, the handleConnectedParticipant() function is called and it creates a new div element for that participant, sets its class and id attributes, and appends it to the webcamFeedContainer element.

This function then iterates through each of the participant's tracks and calls a function named handleTrackPublication() for each track.

This function also sets up an event listener that listens for when a new track is published by the participant and calls a function named handleTrackPublication() when a new track is published.

Add this handleTrackPublication() function below the handleConnectedParticipant() function:

const handleTrackPublication = (trackPublication, participant) => {
  function displayTrack(track) {
    const participantDiv = document.getElementById(participant.identity);
    participantDiv.append(track.attach());
  }

  if (trackPublication.track) {
    displayTrack(trackPublication.track);
  }

  trackPublication.on('subscribed', displayTrack);
  Object.keys(trackPublication);
};

The handleTrackPublication() function checks if the track is already subscribed to, and if it is, it calls the displayTrack() function which takes the track as an argument and attaches it to the participant's div element.

This function also sets up an event listener that listens for when the track is subscribed to and calls the displayTrack() function with the track as an argument.

Now add the handleDisconnectedParticipant() function below the handleTrackPublication() function:

const handleDisconnectedParticipant = (participant) => {
  participant.removeAllListeners();
  const participantDiv = document.getElementById(participant.identity);
  participantDiv.remove();
};

startRoom();

This function stops listening for the disconnected participant by removing all event listeners associated with the participant, using the removeAllListeners() method.

Additionally, this function removes the visual representation of the participant's video and audio tracks from the page by getting the div element associated with the disconnected participant using the participant's identity and removing it from the DOM using the remove() method.

Lastly, the startRoom() function is invoked.

Open your preferred browser, open two tabs and in each tab navigate to the http://localhost:3000/ URL. On both tabs you should see a blank white canvas and two webcam feeds, one for each participant that joined the video room :

Video Room demo

Creating the whiteboard

In this section, you will use the canvas API to create the whiteboard where the players will draw the Pictionary word.

Create a file named whiteboard.js inside the public directory and add the following code to it:

const canvas = document.getElementById('whiteboard');
const context = canvas.getContext('2d');

const btnDraw = document.getElementById('btn-draw');
const btnPencil = document.getElementById('btn-pencil');
const btnEraser = document.getElementById('btn-eraser');
const pencilColor = document.getElementById('pencil-color');
const setColorBtns = document.getElementsByClassName('btn-set-color');

In the first part of the above code, the code gets a reference to the canvas element on the page with the id whiteboard and then gets the 2D drawing context for that canvas. This allows the code to draw and manipulate the canvas. This element will be used as the whiteboard.

In the second part of the above code, the code gets references to various elements on the page such as buttons for start drawing, a pencil, an eraser, and a color picker. These elements will be used to control the drawing tools, erase a drawing, and set and visualize the color of the pencil.

Add the following code to the bottom of your whiteboard.js file:

const txtGeneratedWord = document.getElementById('txt-generated-word');
const txtCountdownTimer = document.getElementById('txt-timer');

let isDrawing = false;
let eraserSelected = false;
let selectedColor = 'black';

In the first part of the above code, the code gets references to text fields on the page, one for the generated Pictionary word and one for the countdown timer. These elements will be used to display the Pictionary word and to show the countdown timer.

In the second part of the above code, the code declares three variables: isDrawing, eraserSelected, and selectedColor. The isDrawing variable will be used to determine if a player is drawing on the canvas. The eraserSelected variable will be used to determine if a player selected the eraser tool. The selectedColor variable will be used to store the pencil color selected by a player.

Add the following code at the bottom of your whiteboard.js file:

const managePencilColor = () => {
  for (let i = 0; i < setColorBtns.length; i++) {
    setColorBtns[i].addEventListener('click', () => {
      selectedColor = setColorBtns[i].style.backgroundColor;
      pencilColor.style.backgroundColor = selectedColor;
    });
  }
};

managePencilColor();

Here, you created and called a function named managePencilColor(). This function allows the user to change the color of the pencil by clicking on the color buttons, and it updates the pencil color in the UI by setting the background color of the element with the id pencil-color to the selected color.

Add the following code at the bottom of your whiteboard.js file:

btnDraw.addEventListener('click', async () => {
  clearCanvas();
});

btnPencil.addEventListener('click', () => eraserSelected = false);
btnEraser.addEventListener('click', () => eraserSelected = true);

const clearCanvas = () => {
  context.clearRect(0, 0, canvas.width, canvas.height);
};

Here, first, the code sets up an event listener on the Draw button that listens for a click event. When the Draw button is clicked, a function named clearCanvas() is called.

After setting an event listener on the Draw button, this code sets up event listeners on the Pencil button and Eraser button that listen for click events.

When the Pencil button is clicked, the eraserSelected variable is set to false and when the Eraser button is clicked, the eraserSelected variable is set to true.

Lastly, the function named clearCanvas() was defined. This function clears the entire canvas by using the canvas context's clearRect() method.

Add the following code below the clearCanvas() function:

const startDrawing = (event) => {
  const rect = canvas.getBoundingClientRect();
  const x = (event.clientX - rect.left) / (rect.right - rect.left) * canvas.width;
  const y = (event.clientY - rect.top) / (rect.bottom - rect.top) * canvas.height;
  isDrawing = true;
  context.beginPath();
  context.moveTo(x, y);
};

Here, you created a function named startDrawing(). This function starts a new drawing operation on the canvas by calculating the position of the mouse pointer, setting the isDrawing variable to true, starting a new drawing path, and moving the pencil to the position of the mouse pointer on the canvas, so the local player can start drawing on the canvas.

Add the following code below the startDrawing()  function:

const draw = (event) => {
  if (isDrawing) {
    const rect = canvas.getBoundingClientRect();
    const x = (event.clientX - rect.left) / (rect.right - rect.left) * canvas.width;
    const y = (event.clientY - rect.top) / (rect.bottom - rect.top) * canvas.height;

    context.strokeStyle = !eraserSelected ? selectedColor : 'white';
    context.lineTo(x, y);
    context.stroke();
  }
};

In the block of code above, you created a function named draw(). This function will only allow the player to draw on the canvas if the value stored in the isDrawing variable is equal to true.

The draw() function allows the player to draw on the canvas by calculating the position of the mouse pointer, setting the color of the pencil, and creating a line from the previous position of the pencil to the current position of the mouse pointer on the canvas, and then draws this line on the canvas.

Additionally, this function checks if the value stored in the eraserSelected variable is equal to true. If that is the case,  the pencil color will be set to white and since white is the canvas background color this will produce the erasing effect.

Add the following code to the bottom of whiteboard.js:

const stopDrawing = () => {
  isDrawing = false;
};

This function stops the current drawing operation by setting the isDrawing variable to false, so the draw() function will no longer execute when the user moves the mouse pointer on the canvas.

Add the following code below the stopDrawing() function:

canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);

The code above sets up event listeners on the canvas element that listen for mouse events such as mousedown, mousemove, and mouseup.

When the mousedown event is triggered (when the user presses down on the mouse button while the cursor is over the canvas), the startDrawing() function is called.

When the mousemove event is triggered (when the user moves the cursor while holding down the mouse button), the draw() function is called.

When the mouseup event is triggered (when the user releases the mouse button), the named stopDrawing() function is called.

Together, these event listeners allow for drawing on the canvas by starting a drawing operation when the mouse button is pressed down, continuing the drawing operation as the cursor is moved, and ending the drawing operation when the mouse button is released.

Make sure your server is running in your terminal. Then go back to your browser, and refresh the tabs running this application. On both tabs use a different pencil color to draw on each tab and watch how the whiteboard is not being shared:

Private whiteboard

Sharing the whiteboard

In this section, you will use the Twilio Sync SDK to share the whiteboard with all the players.

Go back to your whiteboard.js file and add the following code below the line where you declared the selectedColor variable, around line 15 towards the top of the file:

let twilioSyncClient;
let lastX;
let lastY;
let buffer = [];
const bufferInterval = 100;
let bufferTimer;

This code declares and initializes several variables, these variables are used for synchronizing the drawing state between the players, maintaining the position of the pencil on the canvas, and buffering the drawing state at regular intervals.

Add the highlighted lines to the bottom of the startDrawing() function:

const startDrawing = (event) => {
  …

  lastX = x;
  lastY = y;
};

Here, you set the value for the variables lastX and lastY to x and y respectively. These variables are used to store the last x and y coordinates of the mouse pointer on the canvas. They are used to keep track of the position of the local player's pencil when drawing on the canvas.

Add the highlighted code inside the draw() function below the context.stroke(); line:

const draw = (event) => {
  if (isDrawing) {

    …

    context.stroke();

    const line = {
      x1: lastX, y1: lastY, x2: x, y2: y, color: context.strokeStyle,
    };
  
    buffer.push(line);

    clearTimeout(bufferTimer);
    bufferTimer = setTimeout(flushBuffer, bufferInterval);
    lastX = x;
    lastY = y;
  }
};

The above code records the lines that are being drawn by the local player on the canvas and stores them in the buffer array at regular intervals and updates the lastX, and lastY variables.

Add the highlighted line of code to the bottom of the stopDrawing() function:

const stopDrawing = () => {
  isDrawing = false;
  flushBuffer();
};

In the line added above, a function named flushBuffer() was called. The flushBuffer() function is responsible for sending the buffered data to remote players connected to the same room so that their canvas can be updated with the new drawing.

Add the following code below the mouseup event listener at the bottom of the whiteboard.js file:

const checkAccessToken = () => {
  let nrOfTries = 0;

  const interval = setInterval(() => {
    if (accessToken !== undefined) {
      clearInterval(interval);
      startSharingTheCanvas();
    } else {
      if (nrOfTries === 20) {
        clearInterval(interval);
      }
      nrOfTries += 1;
    }
  }, 1000);
};

checkAccessToken();

Here, you defined and called a function named checkAccessToken(). This function is checking if the access token retrieved in the index.js file is available and if it is, then it calls a function named  startSharingTheCanvas(), otherwise, it continues to check for the access token every 1 second for a total of 20 seconds. If the token is not available after 20 seconds, it stops the checking.

To create this new startSharingTheCanvas() function, copy and paste the following code at the bottom of the whiteboard.js file.

const startSharingTheCanvas = () => {
  twilioSyncClient = new Twilio.Sync.Client(accessToken);

  twilioSyncClient.document('canvas')
    .then((document) => {
      document.on('updated', (event) => {
        if (!event.isLocal) {
          switch (event.data.action) {
            case 'draw':
              syncCanvas(event.data);
              break;
            default:
              break;
          }
        }
      });
    })
    .catch((error) => {
      console.error('Unexpected error', error);
    });
};

This function uses the Twilio Sync library to share the canvas with other players in the same room.

It creates a new instance of the Twilio Sync client and listens for updates to the 'canvas' document. It checks if the update was not made locally, and if that is the case it will apply the update to the canvas by calling a function named  syncCanvas().

This function contains a switch statement that checks the value passed in the action property.

This code uses the action property to set the action that should be performed in the remote player client application. There will be two types of actions: draw and start-timer.

The first action will be used to tell the remote player client application to update the canvas with the values stored in the data property.

The second action will be used to start the timer in the remote player client application. You will create this action handler in the next section.

Add the following code below the startSharingTheCanvas() function:

const syncCanvas = (data) => {
  const lines = data.content;
  for (let i = 0; i < lines.length; i++) {
    context.beginPath();
    context.strokeStyle = lines[i].color;
    context.moveTo(lines[i].x1, lines[i].y1);
    context.lineTo(lines[i].x2, lines[i].y2);
    context.stroke();
  }
};

This code defines a function syncCanvas() that takes in a data object as an argument. This function is used to apply updates made by remote players to the canvas.

The syncCanvas() function loops through the lines array stored in the data received, and for each line, it sets the color, starting point, and end point, and then it draws the line on the canvas.

Add the following code below the syncCanvas() function:

const flushBuffer = () => {
  if (buffer.length === 0) return;
  updateDocument(buffer, 'draw');
  buffer = [];
};

This code defines a function flushBuffer() which is responsible for flushing the buffer of lines that were drawn on the canvas by the user.

If the buffer is not empty, it calls a function named updateDocument(), passing in the buffer of lines, and the string draw as arguments, and then it clears the buffer.

Add the following code below the flushBuffer() function:

const updateDocument = (data, action) => {
  twilioSyncClient.document('canvas').then((doc) => {
    doc.update({ content: data, action });
  });
};

This code defines a function named updateDocument() which takes in two arguments,        data, and action.

This function is used to update the canvas document on Twilio's servers. The update function sends the data and action properties to the Twilio server, which then updates the document with the new data.

Go back to your browser, and refresh the tabs running this application. Use a different pencil color to draw on each tab and watch the whiteboard being updated with both colors:

Shared whiteboard demo

Adding a word generator and a timer

In this section, you will add a word generator and timer to the application. In this section, you will also restrict the number of players drawing on the whiteboard to one.

Open the twilio-pictionary-boilerplate/server.js file and add the following code above the findOrCreateRoom() function (located around line 22):

const pictionaryWords = ['Boat', 'Airplane', 'Car', 'Motorcycle', 'Train', 'Submarine', 'Spaceship', 'Bicycle'];

Here, you created an array named pictionaryWords, which contains the Pictionary words that will be sent to the player that has to draw.

Add the following code below the /join-room endpoint, near the end of the file:

app.get('/generate-pictionary-word', (req, res) => {
  if (pictionaryWords.length > 0) {
    const index = Math.floor(Math.random() * pictionaryWords.length);
    const word = pictionaryWords[index];
    pictionaryWords.splice(index, 1);
    res.send({
      word,
    });
  } else {
    res.send({
      word: 'There are no more pictionary words',
    });
  }
});

This code creates an endpoint named /generate-pictionary-word that, when accessed, returns a random word from the Pictionary words array. If the array is empty, it sends a response with a message stating that There are no more Pictionary words.

Save and close server.js.

Go back to the whiteboard.js file and add the following variables below the line where you declared the variable named bufferTimer toward the top of the file, with all the other variable declarations:

let isMyTurnToDraw = false;
let timeLeft = 0;

The isMyTurnToDraw variable will be used to restrict the number of players drawing to one. The isTimeLeft variable will be used to keep track of the time left on the timer.

Replace the contents inside the Draw button click event listener (around line 37 in whiteboard.js) with the following:

btnDraw.addEventListener('click', async () => {
  const response = await fetch('/generate-pictionary-word', {
    method: 'GET',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
  });
  const { word } = await response.json();
  txtGeneratedWord.innerText = word;

  isMyTurnToDraw = true;
  startTimer();
  updateDocument('', 'start-timer');
});

With the code above, when the Draw button is clicked, it sends a GET request to the server endpoint /generate-pictionary-word using the fetch API.

The server will respond with a JSON object that contains a randomly generated Pictionary word. The response is then parsed and the word is set as the inner text of the HTML element with the id txtGeneratedWord.

It also sets the variable isMyTurnToDraw to true, calls a function named startTimer() to start the timer, and sends a request to update the Twilio document with the action start-timer by calling the updateDocument() function.

Edit the if statement inside draw() function (around line 72) to reflect the highlighted line:

const draw = (event) => {
  if (isDrawing && isMyTurnToDraw && timeLeft > 0) {
    …
  }
};

Now the code checks if the user is currently drawing, if it is their turn to draw, and if the countdown timer for their turn has not expired, before allowing them to draw on the canvas. If all these conditions are met, the user can draw on the canvas.

Add the highlighted condition to the switch statement inside the startSharingTheCanvas() function (function starts around line 122 in whiteboard.js):

 
const startSharingTheCanvas = () => {
  … 

        if (!event.isLocal) {
          switch (event.data.action) {
            case 'draw':
              syncCanvas(event.data);
              break;
            case 'start-timer':
              txtGeneratedWord.innerText = '';
              startTimer();
              break;
            default:
              break;
          }
        }

  …
};

This code adds a handler for the start-timer action. When the action is start-timer, it calls a function called startTimer() and clears the element that displays the generated word in the remote player client application.

Create this new startTimer() function now by adding the following code to the bottom of the whiteboard.js file:

const startTimer = () => {
  clearCanvas();
  timeLeft = 60;
  btnDraw.disabled = true;
  const interval = setInterval(() => {
    if (timeLeft > -1) {
      txtCountdownTimer.innerText = timeLeft;
      timeLeft -= 1;
    } else {
      clearInterval(interval);
      btnDraw.disabled = false;
      isMyTurnToDraw = false;
    }
  }, 1000);
};

The startTimer() function is responsible for starting a 60 second count-down timer when a player is drawing a Pictionary word.

When this function is called the Draw button is disabled, the canvas is cleared and the time left to draw is displayed in the txtCountdownTimer DOM element.

While the Draw button is disabled, only the player who clicked it first will be able to draw the Pictionary word on the canvas while the drawing is being displayed to the other player.

Once the timer stops the Draw button will be enabled and any player will be able to click on it to draw another Pictionary word.

Go back to your browser, and refresh the tabs running this application. In one of the tabs click the Draw button, once the timer starts try to draw the Pictionary word, and watch the whiteboard be updated on the other tab:

Demo of the complete Pictionary web app

Conclusion

In this tutorial, you learned how to use Twilio Video and Sync APIs to build a Pictionary web app. First, you learned how to use the Twilio Video API to create a video room where players could join and communicate with each other.  After creating the video room, you learned how to use the Canvas API to create a whiteboard where the players can draw pictures.  Lastly, you learned how to use the Twilio Sync API to share the whiteboard with all the players.

The code for the entire application is available in the following repository https://github.com/CSFM93/twilio-pictionary-complete.

Carlos Mucuho is a Mozambican geologist turned developer who enjoys using programming to bring ideas into reality. https://twitter.com/CarlosMucuho