DEV Community

HarmonyOS
HarmonyOS

Posted on

How to Build a Real-Time Collaborative Canvas Using WebSockets in HarmonyOS (ArkTS/ArkUI)

Read the original article:How to Build a Real-Time Collaborative Canvas Using WebSockets in HarmonyOS (ArkTS/ArkUI)

Introduction
In this tutorial, we’re going to build a real-time collaborative whiteboard (blackboard in our case) app for HarmonyOS using ArkTS/ArkUI and WebSockets.

The app will let multiple users draw on a shared canvas at the same time, with every stroke syncing live across devices via a simple WebSocket server.

Here is what we are going to build:

Required Dependencies

For the WebSocket server:

  • Node.js
  • ws WebSocket library

Install with:

npm init -y
npm install ws
Enter fullscreen mode Exit fullscreen mode

For the HarmonyOS ArkTS app:

  • HarmonyOS 4+ SDK
  • ArkTS/ArkUI project in DevEco Studio
  • Required permission in module.json5:
"requestPermissions": [
  { "name": "ohos.permission.INTERNET"}
]
Enter fullscreen mode Exit fullscreen mode

WebSocket Server Implementation
Our server will listen for drawing events from clients and broadcast them to all other connected clients in real-time.

File: server.js:

// Import the ws library
const WebSocket = require('ws');
// Create WebSocket server listening on port 8000
const wss = new WebSocket.Server({ port: 8000 });
wss.on('connection', (ws) => {
    console.log('Client connected');

    // Listen for incoming messages from this client
    ws.on('message', (message) => {
        try {
            const data = JSON.parse(message);

            // Handle "draw" events by broadcasting to other clients
            if (Array.isArray(data) && data[0] === 'draw') {
                wss.clients.forEach(client => {
                    if (client !== ws && client.readyState === WebSocket.OPEN) {
                        client.send(JSON.stringify(data));
                    }
                });
            }

            // Handle "clear" events by telling everyone to clear their canvas
            if (Array.isArray(data) && data[0] === 'clear') {
                wss.clients.forEach(client => {
                    if (client !== ws && client.readyState === WebSocket.OPEN) {
                        client.send(JSON.stringify(['clear']));
                    }
                });
            }
        } catch (err) {
            console.error('Invalid JSON:', err);
        }
    });
});

// Server ready event
wss.on('listening', () => {
    console.log('WebSocket is listening on port 8000');
});

// Handle errors
wss.on('error', (error) => {
    console.error('WebSocket server error:', error);
});
Enter fullscreen mode Exit fullscreen mode

How it works:

  • Clients send either [“draw”, strokeData] or [“clear”] messages.
  • Server relays them to all other connected clients in real time. Start the server with:
node server.js
Enter fullscreen mode Exit fullscreen mode

ArkTS Project

Next, let’s build the HarmonyOS app that:

✅ Connects to the WebSocket server

✅ Lets the user draw on a canvas

✅ Sends strokes to the server

✅ Receives strokes from others and draws them

We’ll use:

  • ArkUI’s Canvas component to render strokes.
  • @kit.NetworkKit to manage WebSocket connections.

WebSocket Configuration

import { webSocket } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';

// Emulator localhost address
const SOCKET_URL = "ws://10.0.2.2:8000";
Enter fullscreen mode Exit fullscreen mode

💡 Note: If app is on emulator we need to use 10.0.2.2 instead of localhost to connect to your development machine.

State and WebSocket Connection

@State strokes: Stroke[][] = [[]];  // Collection of all drawn strokes
private socket: webSocket.WebSocket | undefined;

aboutToAppear(): void {
  this.connectWebSocket();
}

connectWebSocket() {
  this.socket = webSocket.createWebSocket();

  // When the connection opens
  this.socket.on('open', (err, value) => {
    console.log("[SOCKET] Connected to server");
  });

  // Handle incoming messages
  this.socket.on('message', (err, value) => {
    if (typeof value === 'string') {
      const response = JSON.parse(value);

      if (response[0] === 'draw') {
        const newStroke: Stroke = response[1];
        this.strokes[this.strokes.length - 1].push(newStroke);
        this.redraw();
      }

      if (response[0] === 'clear') {
        this.strokes = [[]];
        this.redraw();
      }
    }
  });

  // Connect to the WebSocket server
  this.socket.connect(SOCKET_URL, (err, value) => {
    console.log(err ? "[SOCKET] Connection failed" : "[SOCKET] Connected successfully");
  });
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Opens a WebSocket connection on app start.
  • Receives draw or clear events and updates the local canvas.

Canvas Setup and Touch Handling

// Prepare the canvas background and border
onCanvasReady() {
  this.context.fillStyle = '#000'; // Black background
  this.context.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

  this.context.strokeStyle = '#888';
  this.context.lineWidth = 2;
  this.context.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

  this.context.strokeStyle = '#FFFFFF'; // Drawing color
  this.context.lineWidth = 2;
}

// Handle user touch input
onCanvasTouch(event: TouchEvent) {
  const newStroke: Stroke = {
    x: event.touches[0].x,
    y: event.touches[0].y
  };

  // Save stroke locally
  this.strokes[this.strokes.length - 1].push(newStroke);

  // Send stroke to other clients
  this.sendDraw(newStroke);

  // Update canvas
  this.redraw();
}
Enter fullscreen mode Exit fullscreen mode

Sending Drawing Events

// Send the user's stroke to the server
sendDraw(stroke: Stroke) {
  if (this.socket) {
    const payload = JSON.stringify(["draw", stroke]);
    this.socket.send(payload, (err) => {
      if (!err) {
        console.log("[SOCKET] Stroke sent successfully");
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Clearing the Canvas

// Clear the local canvas and notify others
clearCanvas() {
  this.strokes = [[]];

  if (this.socket) {
    const payload = JSON.stringify(['clear']);
    this.socket.send(payload, (err) => {
      if (!err) {
        console.log("[SOCKET] Cleared canvas");
      }
    });
  }

  this.redraw();
}
Enter fullscreen mode Exit fullscreen mode

Redrawing the Canvas

// Redraw all strokes from history
redraw() {
  this.context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  this.onCanvasReady();

  this.strokes.forEach(path => {
    if (path.length < 2) return;

    this.context.beginPath();
    this.context.moveTo(path[0].x, path[0].y);
    for (let i = 1; i < path.length; i++) {
      this.context.lineTo(path[i].x, path[i].y);
    }
    this.context.stroke();
  });
}
Enter fullscreen mode Exit fullscreen mode

Building the UI

build() {
  Column() {
    // Canvas area for drawing
    Canvas(this.context)
      .onTouch((event) => this.onCanvasTouch(event))
      .onReady(() => this.onCanvasReady())
      .width(CANVAS_WIDTH)
      .height(CANVAS_HEIGHT)

    Blank()

    // Button to clear the canvas
    Button('Clear')
      .onClick(() => this.clearCanvas())
  }
  .height('100%')
  .width('100%')
  .alignItems(HorizontalAlign.Center)
  .justifyContent(FlexAlign.Center)
}
Enter fullscreen mode Exit fullscreen mode

Pro Tip
✨ Emulator Networking Tip:

When testing in DevEco Studio’s Android-based emulator, always use 10.0.2.2 instead of localhost or 127.0.0.1 to access your development machine’s localhost.

On physical devices, ensure both your server and the device are on the same local network.

Conclusion
And there you have it — a complete real-time collaborative drawing app for HarmonyOS built with ArkTS and ArkUI!

You learned to:

✅ Create a Node.js WebSocket server.

✅ Handle drawing events on a canvas in ArkTS.

✅ Sync drawing data across devices in real-time.

✨ Check out the GIF above to see how it all works in action!

Mastering WebSockets and Canvas in HarmonyOS unlocks tons of collaborative app ideas. Happy coding!

Written by Taskhyn Maksim

Top comments (0)