<head>
<meta charset="utf-8" />
<title>Shared Whiteboard Powered by Amazon IVS</title>
</head>
<body style="padding: 10px;">
  <div class="card mb-3" id="settings">
    <div class="card-header bg-dark text-white">
      Settings
    </div>
    <div class="card-body">
      <div class="mb-3 row">
        <label for="chat-userid" class="col-sm-2 col-form-label">Chat UserId</label>
        <div class="col-sm-10">
          <input type="text" class="col-sm-10 form-control" id="chat-userid" required />
        </div>
      </div>
      <div class="mb-3 row">
        <label for="pen-color" class="col-sm-2 col-form-label">Pen Color</label>
        <div class="col-sm-10">
          <input type="color" class="form-control form-control-color" id="pen-color" required />
        </div>
      </div>
      <div class="mb-3 row">
        <label for="chat-token" class="col-sm-2 col-form-label">Chat Token</label>
        <div class="col-sm-10">
          <input type="text" class="col-sm-10 form-control" id="chat-token" required />
        </div>
      </div>
      <div class="mb-3 row">
        <label for="chat-endpoint" class="col-sm-2 col-form-label">Endpoint</label>
        <div class="col-sm-10">
          <input type="text" class="form-control" id="chat-endpoint" placeholder="Begins with: wss://" value="wss://edge.ivschat.us-east-1.amazonaws.com" required />
        </div>
      </div>
      <div class="mb-3 row">
        <div class="col-sm-10 offset-sm-2">
          <button type="submit" class="btn btn-dark" id="submit-settings">Submit</button>  
        </div>
      </div>
    </div>
  </div>
  <div id="whiteboard-container" class="w-100 vh-100">
    <canvas id="whiteboard" class="w-100 h-100 border rounded border-dark shadow">
      Sorry, your browser does not support HTML5 canvas technology.
    </canvas>  
  </div>
</body>
</html>
#whiteboard {
	cursor: crosshair;
}
window.isDrawing = false;
window.chatEndpoint;
window.chatToken;
window.userId;
window.penColor;
window.connection;
window.throttlePause;
window.queue = [];
window.maxQueueSize = 20;

const throttle = (callback, time) => {
  if (window.throttlePause) return;
  window.throttlePause = true;
  setTimeout(() => {
    callback();
    window.throttlePause = false;
  }, time);
};

const handleQueue = (event) => {
  if(window.queue.length <= window.maxQueueSize) {
    window.queue.push(event);
  }
  if(window.queue.length === window.maxQueueSize || event.type == 'mouseup') {
    sendEvents();
  }
};

const sendEvents = () => {
  const payload = {
    'Action': 'SEND_MESSAGE',
    'Content': '[whiteboard event]',
    'Attributes': {
      'type': 'whiteboard',
      'color': window.penColor,
      'events': JSON.stringify(window.queue),
    }
  }
  try {
    window.connection.send(JSON.stringify(payload));
    window.queue = [];
  }
  catch(e) {
    console.error(e);
  }
}
const onMouseDown = (e) => {
  const canvasEl = document.getElementById('whiteboard');
	const ctx = canvasEl.getContext('2d');
  ctx.beginPath();
  const x = e.x;
  const y = e.y;
  ctx.moveTo(x, y);
};

const onMouseMove = (e, color) => {
  const canvasEl = document.getElementById('whiteboard');
  const ctx = canvasEl.getContext('2d');
  const x = e.x;
  const y = e.y;
  ctx.lineTo(x, y);
  ctx.strokeStyle = color || window.penColor;
  ctx.stroke();
};

const onMouseUp = (e) => {
  const canvasEl = document.getElementById('whiteboard');
  const ctx = canvasEl.getContext('2d');
  // any necessary actions
  // such as:
  // ctx.closePath();
}

const init = () => {
  window.chatEndpoint = document.getElementById('chat-endpoint').value;
  window.userId = document.getElementById('chat-userid').value;
  window.chatToken = document.getElementById('chat-token').value;
  window.penColor = document.getElementById('pen-color').value;
  if(!window.chatEndpoint || !window.chatToken || !window.userId) {
    alert('Chat Endpoint, Token and UserId are required!');
    return;
  }
  document.getElementById('settings').classList.add('d-none')
  
  window.connection = new WebSocket(window.chatEndpoint, window.chatToken);
  window.connection.addEventListener('message', (e) => {
    const data = JSON.parse(e.data);
    const msgType = data.Attributes.type;
    
    if(msgType == 'whiteboard') {
      const events = JSON.parse(data.Attributes.events);
      const color = data.Attributes.color;
      const eventUserId = data.Sender.UserId;
      events.forEach(e => {
        const type = e.type;
        if(eventUserId != window.userId) {
          switch(type){
            case 'mousedown':
              onMouseDown({x: e.x, y: e.y});
              break;
            case 'mousemove':
              onMouseMove({x: e.x, y: e.y}, color);
              break;
            case 'mouseup':
              onMouseUp({});
              break;
          };  
        }      
      });  
    }
    
    // otherwise, handle as an incoming chat...
  });
  const whiteboardContainer = document.getElementById('whiteboard-container');
	const canvasEl = document.getElementById('whiteboard');
  canvasEl.width = whiteboardContainer.offsetWidth;
	canvasEl.height = whiteboardContainer.offsetHeight;
	const ctx = canvasEl.getContext('2d');
  ctx.lineWidth = 5;
  ctx.fillStyle='#fff';
  ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
	
	if(canvasEl){
    canvasEl.addEventListener('mousedown', (e) => {
      window.isDrawing = true;
      const evt = {x: e.offsetX, y: e.offsetY, type: 'mousedown'};
      onMouseDown(evt);
      handleQueue(evt);
    });
    canvasEl.addEventListener('mousemove', (e) => {
      if(window.isDrawing) {
        const evt = {x: e.offsetX, y: e.offsetY, type: 'mousemove'};
        throttle(() => {
          handleQueue(evt);
        }, 50);
        onMouseMove(evt);
      }
    });
    canvasEl.addEventListener('mouseup', (e) => {
      window.isDrawing = false;
      onMouseUp({});
      handleQueue({type: 'mouseup'});
    });
	}
	
};

document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('pen-color').value = `#${Math.floor(Math.random()*16777215).toString(16)}`;
  document.getElementById('submit-settings').addEventListener('click', () => {
    init();
  })
});

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.2/css/bootstrap.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.