Node.js socket.io simple drawing example

Made by Marvin Drude

Node.js socket.io simple drawing example

Introduction

In the following article I am going to show you how to setup a simple Node.js application with socket.io and express in order to have live bidirectional communication between http client (Browser) and your server. You should already have a basic understanding of JavaScript, HTML, CSS for this tutorial.

Requirements

In order to follow along with this tutorial, you will need to setup a new Node.js project in which you install these modules with npm install.

npm install <module_name>
  • express
  • socketio
  • http

Directory Structure

The next step is to create the needed directories and files to work with. The following tree shows the structure I like to use and will take for granted.

  • node_modules
  • public
    • index.html
    • client.js
    • socketio.js
    • style.css
  • app.js
  • package-lock.json

Server-side programming

Now let’s start with the backend/server-side programming. In the beginning we need to import the modules we installed with npm install. Everything related to the server goes into “app.js” for now.

// General imports to use
const express       = require("express");
const http          = require("http");
const socketio      = require("socket.io");

After we have successfully imported the modules with the require() function provided by Node.js, we need to initialize the required objects in order to listen to incoming http requests and web socket connections.

// Initialization of express and socket io
const app           = express();
const server        = http.Server(app);
const io            = socketio(server);

The “app” and “server” are only used to serve the client requesting our page the index.html file as seen below. After adding the code below and having created the index.html in the folder public you could already test run the server by using the command “node app.js”. You should see an empty page opening localhost:3000 in the browser.

// Serve index.html to any request
app.use("/", express.static(__dirname + '/public/'));

// Listen to HTTP requests on port 3000
server.listen(3000, () => { });

Now we are finally ready to start programming the logic of the socket io server. We use the “io” object created above in order to listen to new socket connections.

// Client connected to web socket
io.on("connection", (socket) => {

});

Next will be the required logic of the server. The purpose of the server is only to accomplish two tasks at the moment. It sends the latest drawing state to new connections and sends new drawing positions to every client that is connected at that moment.

const drawState = {
    "positions": []
};

// Client connected to web socket
io.on("connection", (socket) => {

    // Send new clients the current state of the drawing
    socket.emit("init-draw", drawState.positions);

    // When client is drawing, it sends every new position to the server
    socket.on("draw", (position) => {

        // Add new position to current state
        drawState.positions.push(position);

        // Send the new position to draw to every client that is connected
        io.emit("new-draw-position", position);

    });

});

Client-side programming

However, let’s get started with the client-side code in order to make something appear on your screen. Here you can see the complete index.html.

<!DOCTYPE html>
<html>
    <head>
        <title>Node.js Socket.io Example</title>
        <link rel="stylesheet" href="style.css" />
    </head>
    <body>
        <!-- Used to render drawing -->
        <canvas id="drawing"></canvas>
        <!-- Socket.io library -->
        <script src="socketio.js"></script>
        <!-- Our custom client code-->
        <script src="client.js"></script>
    </body>
</html>

The style.css is very small and is only responsible to make the canvas take on the whole page size.


* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

body {
    overflow: hidden;
}

#drawing {
    display: block;
    width: 100vw;
    height: 100vh;
}

Last but not least here is the client.js with comments to explain everything.


// default namespace used
var example = { };

(function() {

    // define client class
    var Client = function Client() {

        // connect to socketio server we have just setup
        this.socket = io.connect();
        // find canvas in html nodes
        this.canvas = document.getElementById("drawing");
        // get canvas context 2d in order to draw on the canvas
        this.ctx = this.canvas.getContext("2d");

        this.canvas.width = document.body.clientWidth;
        this.canvas.height = document.body.clientHeight;

        this.initHandlers();
        this.initSocketHandlers();

    }

    // register event handlers
    Client.prototype.initHandlers = function() {

        // indicates if user is drawing at the moment
        this.drawing = false;
        // start position of line
        this.start = {
            "x": 0,
            "y": 0
        };
        // current mouse position
        this.current = {
            "x": 0,
            "y": 0
        };

        this.canvas.addEventListener("mousedown", function(e) {

            this.drawing = true;
            this.start.x = this.current.x;
            this.start.y = this.current.y;

        }.bind(this));

        this.canvas.addEventListener("mouseup", function(e) {

            this.drawing = false;

        }.bind(this));

        this.canvas.addEventListener("mousemove", function(e) {

            // set current position
            this.current.x = e.clientX;
            this.current.y = e.clientY;

            if(this.drawing) {

                // send data to server, normalize data if others have other browser size
                this.socket.emit("draw", {
                    "start": {
                        "x": this.start.x / this.canvas.width,
                        "y": this.start.y / this.canvas.height
                    },
                    "current": {
                        "x": this.current.x / this.canvas.width,
                        "y": this.current.y / this.canvas.height
                    }
                });

                // set start to current position
                this.start.x = this.current.x;
                this.start.y = this.current.y;

            }

        }.bind(this));

    }

    // init socket handlers
    Client.prototype.initSocketHandlers = function() {

        // new line position sent by the server
        this.socket.on("new-draw-position", function(data) {
            this.drawLine(data);
        }.bind(this));

        // draw all lines, which were drawn before you entered the site
        this.socket.on("init-draw", function(arr) {
            for(var e = 0; e < arr.length; e++) {
                this.drawLine(arr[e]);
            }
        }.bind(this));

    }

    // draw line on canvas
    Client.prototype.drawLine = function(data) {

        // multiply normalized data with canvas size
        this.ctx.beginPath();
        this.ctx.moveTo(data.start.x * this.canvas.width, data.start.y * this.canvas.height);
        this.ctx.lineTo(data.current.x * this.canvas.width, data.current.y * this.canvas.height);
        this.ctx.strokeStyle = "#000000";
        this.ctx.stroke();

    }
    
    example.Client = Client;

})();

// init logic on document load
window.addEventListener("load", function() {

    // create new client object
    new example.Client();

});
Drawn and displayed on two different browsers

Possibilities are endless

There are so many things you can improve and extend. Here are just a small portion of them.

  • Different colors, sizes
  • Reset the canvas
  • Different shapes
  • Show cursor positions of others
  • Resize canvas