The Interactive & Immersive HQ

How to Manage Multiple WebSocket Connections in TouchDesigner

In this tutorial we will show how to integrate multiple WebSocket connections to use in interactive projects in TouchDesigner.

In this post we learned about WebSocket and its importance in the development of immersive and interactive environments.

Today we will learn how to create a custom web interface and use it to manipulate data in real time in TouchDesigner. And, most, of all, multiple users will be able to interact simultaneously with our lovely installations. I am guessing you are already planning how to use it. So let’s go!

Just to sum up, WebSocket is is a bidirectional, full-duplex protocol for client-server communication over a single TCP connection.

The project is split in three parts:

Create a WebSocket Server

The first thing we need to do is to establish a WebSocket server, that will be in charge of the bi-directional data communication.

There are plenty of options to do this. I started from the precious work of Torin Blankensmith, he published a dedicated Youtube playlist as well as a WebSocket node server template.

In order to create the server:

Now we need to create the WebSocket server. There are plenty of available services around the web. I decided to use the cloud application platform Heroku. So:

  • Go to Heroku website and create an account
  • Click on Create new app
  • Choose a name for your app and select region
  • Click on Create app

In the next screen, in the deployment method section, click on GitHub, select the repository fork you just created on GitHub and click Connect.

Congrats, we just created our WebSocket server! Now let’s go to the UI side.

Get Our 7 Core TouchDesigner Templates, FREE

We’re making our 7 core project file templates available – for free.

These templates shed light into the most useful and sometimes obtuse features of TouchDesigner.

They’re designed to be immediately applicable for the complete TouchDesigner beginner, while also providing inspiration for the advanced user.

Create the UI

For this project, we want to be able to control parameters in TouchDesigner via a custom web interface, for example a joystick. The interface data, such as X and Y coordinates, speed and angle, will be streamed to TouchDesigner via WebSocket in real time.

We created a Pen on CodePen that allows us to code and see the result directly in the browser. We started from this code example and customized it. For sake of speed, we put the Javascript script into the HTML code, but of course you can create a separate JS file and recall it via script in the HTML code.

Let’s have a look at the code.

<!DOCTYPE html>
<html>

<head>
  <title>Mousebot</title>
  <meta name="viewport" content="user-scalable=no">
</head>

<body style="position: fixed; font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif ; color:rgb(128, 128, 128); font-size: large;">
  <h2 style="text-align:center">Joystick</h2>
  <p style="text-align: center;">
    X: <span id="x_coordinate"></span>
    Y: <span id="y_coordinate"></span>
    Speed: <span id="speed"></span> %
    Angle: <span id="angle"></span>
  </p>
  <canvas id="canvas" name="game"></canvas>
  <script>
    var connection = new WebSocket('wss://super-synth-63d47bc15387.herokuapp.com');
    var sliderId = Math.floor(Math.random() * 1000);
    var lastDataSentTime = Date.now();
    const timeoutDuration = 10000; // 10 secondi

    connection.onopen = function() {
      connection.send(JSON.stringify({
        type: 'connect',
        sliderId: sliderId
      }));
    };

    connection.onerror = function(error) {
      console.log('WebSocket Error', error);
      alert('WebSocket Error', error);
    };

    connection.onmessage = function(e) {
      console.log('Server:', e.data);
    };

    function send(x, y, speed, angle) {
      var data = {
        "x": x,
        "y": y,
        "speed": speed,
        "angle": angle,
        "sliderId": sliderId
      };
      data = JSON.stringify(data);
      console.log(data);
      connection.send(data);
      lastDataSentTime = Date.now(); // Update the last data submission time
    }

    function checkTimeout() {
      const currentTime = Date.now();
      if (currentTime - lastDataSentTime > timeoutDuration) {
        // It has been more than 10 seconds since the last data submission
        sendDeleteCommand();
        lastDataSentTime = Date.now(); // Timer reset
      }
    }

    function sendDeleteCommand() {
      var data = {
        "type": "delete",
        "sliderId": sliderId
      };
      data = JSON.stringify(data);
      console.log('Sending delete command:', data);
      connection.send(data);
    }

    setInterval(checkTimeout, 1000); // Check every second

    var canvas, ctx, width, height, radius, x_orig, y_orig;

    window.addEventListener('load', () => {
      canvas = document.getElementById('canvas');
      ctx = canvas.getContext('2d');
      resize();
      document.addEventListener('mousedown', startDrawing);
      document.addEventListener('mouseup', stopDrawing);
      document.addEventListener('mousemove', Draw);
      document.addEventListener('touchstart', startDrawing);
      document.addEventListener('touchend', stopDrawing);
      document.addEventListener('touchcancel', stopDrawing);
      document.addEventListener('touchmove', Draw);
      window.addEventListener('resize', resize);
      document.getElementById("x_coordinate").innerText = 0;
      document.getElementById("y_coordinate").innerText = 0;
      document.getElementById("speed").innerText = 0;
      document.getElementById("angle").innerText = 0;
    });

    function resize() {
      width = window.innerWidth;
      radius = 170;
      height = radius * 3.5;
      ctx.canvas.width = width;
      ctx.canvas.height = height;
      background();
      joystick(width / 2, height / 3);
    }

    function background() {
      x_orig = width / 2;
      y_orig = height / 3;
      ctx.beginPath();
      ctx.arc(x_orig, y_orig, radius + 20, 0, Math.PI * 2, true);
      ctx.fillStyle = '#09803e';
      ctx.fill();
    }

    function joystick(width, height) {
      ctx.beginPath();
      ctx.arc(width, height, radius / 4, 0, Math.PI * 2, true);
      ctx.fillStyle = '#241e1e';
      ctx.fill();
      ctx.strokeStyle = '#6b5e5e';
      ctx.lineWidth = 8;
      ctx.stroke();
    }

    let coord = {
      x: 0,
      y: 0
    };
    let paint = false;

    function getPosition(event) {
      var mouse_x = event.clientX || event.touches[0].clientX;
      var mouse_y = event.clientY || event.touches[0].clientY;
      coord.x = mouse_x - canvas.offsetLeft;
      coord.y = mouse_y - canvas.offsetTop;
    }

    function is_it_in_the_circle() {
      var current_radius = Math.sqrt(Math.pow(coord.x - x_orig, 2) + Math.pow(coord.y - y_orig, 2));
      return radius >= current_radius;
    }

    function startDrawing(event) {
      paint = true;
      getPosition(event);
      if (is_it_in_the_circle()) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        background();
        joystick(coord.x, coord.y);
        Draw(event);
      }
    }

    function stopDrawing() {
      paint = false;
    }

    function Draw(event) {
      if (paint) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        background();
        var angle_in_degrees, x, y, speed;
        var angle = Math.atan2((coord.y - y_orig), (coord.x - x_orig));
        if (Math.sign(angle) == -1) {
          angle_in_degrees = Math.round(-angle * 180 / Math.PI);
        } else {
          angle_in_degrees = Math.round(360 - angle * 180 / Math.PI);
        }
        if (is_it_in_the_circle()) {
          joystick(coord.x, coord.y);
          x = coord.x;
          y = coord.y;
        } else {
          x = radius * Math.cos(angle) + x_orig;
          y = radius * Math.sin(angle) + y_orig;
          joystick(x, y);
        }
        getPosition(event);
        speed = Math.round(100 * Math.sqrt(Math.pow(x - x_orig, 2) + Math.pow(y - y_orig, 2)) / radius);
        var x_relative = Math.round(x - x_orig);
        var y_relative = Math.round(y - y_orig);
        document.getElementById("x_coordinate").innerText = x_relative;
        document.getElementById("y_coordinate").innerText = y_relative;
        document.getElementById("speed").innerText = speed;
        document.getElementById("angle").innerText = angle_in_degrees;
        send(x_relative, y_relative, speed, angle_in_degrees);
      }
    }
  </script>
</body>

</html>

Let’s analyze it:

  • Line 1 to 17: standard code formatting
  • Line 19: we establish the connection with the Heroku WebSocket server. Remember to insert the link to your own server
  • Line 20 to 22: we create a variable called SliderId, that assigns a random ID for each single connected user. We also define two time variables that will be useful for the final outcome, we will see it later on
  • Lines 24 to 29: we open the JSON connection and assign the IDs for each user
  • Lines 40 to 52: we create the main variables – X, Y, angle, speed, sliderId – and establish the JSON connection
  • Lines 54 to 61: we create a checkTimeout function, that resets time if no more data have been received after ten seconds. The following sendDeleteCommand function erase the ID after the ten seconds set interval
  • Lines 75 to 93: we define the UI variables and we create the addEventListener events, in order to send mouse and smartphone touch events such as touchstart, touchend, etc
  • From line 95 onwards we find all the functions needed to create the UI in Javascript

OK, now we have our wonderful UI we can interact with via smartphone or mouse.

Now let’s open TouchDesigner and create the patch.

Manage Multiple WebSocket Connections in TouchDesigner

So, here we have the patch:

Websocket Connections TouchDesigner

The first thing we need to to is to create a DAT WebSocket component. In the Connect tab we insert our network address – the Heroku address – and set the network port to 443.

Now we to edit the WebSocket callback tab in order to manage the connection between the WebSocket server, the TouchDesigner and the web interface.

Here is the code:

import json
import time

# Dictionary tracking update times for each sliderId
last_update_times = {}

def onConnect(dat):
    print("Connected")
    return

def onDisconnect(dat):
    print("Disconnected")
    return

def onReceiveText(dat, rowIndex, message):
    global last_update_times

    if message == "ping":
        dat.sendText("pong")
        return

    data = json.loads(message)
    message_type = data.get('type', 'update')

    if message_type == 'update':
        if 'x' in data and 'y' in data and 'sliderId' in data and 'speed' in data and 'angle' in data:
            x = data['x']
            y = data['y']
            speed = data['speed']
            angle = data['angle']
            sliderId = data['sliderId']
            
            # Update last updated time for the sliderId
            last_update_times[sliderId] = time.time()

            # Update or add the row in the table
            update_table(sliderId, x, y, speed, angle)

    elif message_type == 'delete':
        sliderId = data.get('sliderId')
        if sliderId is not None:
            delete_row(sliderId)

def update_table(sliderId, x, y, speed, angle):
    table = op('test_test')
    
    # Search for the sliderId inside the table
    row_index = None
    for i in range(1, table.numRows):
        if table[i, 'user'] == str(sliderId):
            row_index = i
            break
    
    # Update values if the sliderId already exists
    if row_index is not None:
        table[row_index, 'valuex'] = x
        table[row_index, 'valuey'] = y
        table[row_index, 'speed'] = speed
        table[row_index, 'angle'] = angle
    else:
        # Add a new row in the table if the sliderId does not exists
        table.appendRow([sliderId, x, y, speed, angle])

def delete_row(sliderId):
    table = op('test_test')
    row_index = None
    for i in range(1, table.numRows):
        if table[i, 'user'] == str(sliderId):
            row_index = i
            break
    if row_index is not None:
        table.deleteRow(row_index)
        if sliderId in last_update_times:
            del last_update_times[sliderId]
        print(f"Deleted row for sliderId: {sliderId}")


def onReceiveBinary(dat, contents):
    return

def onReceivePing(dat, contents):
    dat.sendPong(contents)  # send a reply with same message
    return

def onReceivePong(dat, contents):
    return

def onMonitorMessage(dat, message):
    return

Let’s analyze it:

  • First of all we need to import json and time in Python
  • Lines 15 to 20: we create a onReceiveText function and a global variable called last_update_times, that we will use to erase table data if the interface does not send data since ten seconds
  • Lines 22 and 23: we load the JSON messages
  • Lines 25 to 31: we create five JSON updated variables: X and Y coordinates, angle, speed and ID (sliderId)
  • Line 34: we create an array of IDs based on the variable last_update_times global and assign it the time.time() variable
  • Line 37: we call the update_table function that updates a five columns table (sliderId, x, y, speed and angle)
  • Lines 39 to 42 are really important: as we saw in the Javascript code, we count for how many seconds a user does not send data. Here, upon receiving the delete message from the script, the Python function delete the SliderId row that is not more active. This is important because only active users will be able to interact with our project
  • Lines 44 to 62: we fill the test_test DAT Table component. The code looks for the sliderIds inside the table and updates the x, y, angle and speed variables for each ID, thus allowing us to parse the right data for the right ID
  • Lines 64 to 75: we check if the row associated to the ID is active or not. If it is not active, the row is deleted and only active users are held

As you can see, this DAT WebSocket component is the beating heart of our system.

Looking at the other components of the patch, the flow is really simple. We focused on the overall system and not on the creative side. So, in our patch, users can just move a simple sphere on a circular screen. You will be in charge to turn data into wonderful works!

So, data from the DAT WebSocket component fill the table test_test. We pass the table data through a DAT Evaluate component in order to resize data to fit the screen.

Going back to our web interface, you can notice that the Y values of the joystick are specular, to say negative values are in the upper part of the interface and the positive one are in the lower one. In TouchDesigner we can select the Y column of the table via the DAT Select component and pass it on another DAT Evaluate component, where we multiply data * -1. Then we merge and replace the Y to get a fully functional table.

Finally, we use table data to instance the GEO of a sphere TOP component and that’s all!

The Cherry On Top

As you can guess, a point is missing. If we close and open the TouchDesigner patch, the table will include the data received during the previous session. So our screen will display spheres that are no more associated to our current IDs. How to manage it?

You can create an Execute DAT and include the following code:

# me - this DAT
# 
# frame - the current frame
# state - True if the timeline is paused
# 
# Make sure the corresponding toggle is enabled in the Execute DAT.

def onStart():
    # Delete all rows from the table except the first one
    clear_table()

def clear_table():
    global table
    # Get a reference to the table
    table = op('test_test')
    
    # Save the first row of the table
    first_row = table.row(0)
    
    # Delete all rows from the table except the first one
    for i in range(table.numRows-1, 0, -1):
        table.deleteRow(i)
    
    # Set the values of the saved first row
    table.row(0)[:] = first_row

    return

def onCreate():
	return

def onExit():
	return

def onFrameStart(frame):
	return

def onFrameEnd(frame):
	return

def onPlayStateChange(state):
	return

def onDeviceChange():
	return

def onProjectPreSave():
	return

def onProjectPostSave():
	return

Let’s analyze it:

  • We call a clear_table() function in def onStart():
  • Next we create the clear_table() function. The function creates a table global variable, references it to our test_test_table, saves the first row of the table, deletes all rows from the table except the first one and set the values of the saved first row

So, as you will open the patch next time, the table will be clear. That’s nice!

Download the patch

Wrap Up

In this tutorial we created a custom application based on multiple real time WebSocket connections. Now that everything is set up, you can create your custom interface and turn data into lovely elements users can interact with in your immersive installations. The sky is the limit!