Search code examples
javascripthtmlcanvassocket.ioreal-time

two people drawing on same canvas


I am making a real-time paint app in html5 canvas. When a single user draws on the canvas then everything goes fine , but when two users draw at same time , everything gets messed up , for example if one changes color , the color for all client changes , and lines start drawing from one point to other . How can this be fixed ? Thanks , here is my code.

var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
canvas.width="600";
canvas.height="500";
var radius = 10;
var mouse = {x:0,y:0};
var drag = false;
var imageObj = new Image();
  imageObj.onload = function() {
    context.drawImage(imageObj, 20, 20);
 };
  imageObj.src = 'rhino4.png';
$scope.colorChange = function(color){
  Socket.emit("colorChange",color);
};
Socket.on("colorChange",function (color) {
  context.strokeStyle = color;
  context.fillStyle = color;
})
$scope.radiusChange = function(size) {
  Socket.emit("radiusChange",size);
}
Socket.on("radiusChange",function (size) {
  radius = size;
  context.lineWidth = radius*2;
})
context.lineWidth = radius*2;
var putPoint = function (mouse) {
  if(drag){
    context.lineTo(mouse.x,mouse.y)
    context.stroke();
    context.beginPath();
    context.arc(mouse.x,mouse.y,radius,0,Math.PI*2);
    context.fill();
    context.beginPath();
    context.moveTo(mouse.x,mouse.y);
    context.globalCompositeOperation='source-atop';
    context.drawImage(imageObj, 20, 20);
    context.globalCompositeOperation='source-over';
  }
}
Socket.on("putPoint",function (mouse) {
  putPoint(mouse);
});
var engage = function(mouse){
  console.log("in engage",mouse);
  drag = true;
  putPoint(mouse);
}
var disengage = function(){
  drag = false;
  context.beginPath();
}
var socketPutPoint = function(e){
  mouse.x = e.offsetX;
  mouse.y = e.offsetY;
  Socket.emit("putPoint",mouse);
}
Socket.on("engage",function (mouse) {
  console.log("engaging");
  engage(mouse);
});
var socketEngage = function (e) {
  mouse.x = e.offsetX;
  mouse.y = e.offsetY;
  console.log(mouse);
  Socket.emit("engage",mouse);
}
var socketDisengage = function (e) {
  mouse.x = e.offsetX;
  mouse.y = e.offsetY;
  console.log(mouse);
  Socket.emit("disengage",mouse);
}
Socket.on("disengage",function (mouse) {
  disengage();
})
canvas.addEventListener('mouseup',socketDisengage);
canvas.addEventListener('mouseleave',socketDisengage);
canvas.addEventListener('mousedown',socketEngage);
canvas.addEventListener('mousemove',socketPutPoint);

I thought of changing the color back to original in colorChange method after putpoint , but that does not seem to work


Solution

  • Some whiteboarding hints:

    All the following code is pseudo-code!

    • Use websockets for communication. Several popular websocket libraries are SocketIO and SignalR. Websocket libraries often have fallback methods when websockets are not supported.

    • Use JSON to serialize your drawing data. The nice thing about JSON is that it automatically takes JavaScript objects / arrays and makes a string from them that's suitable for websocket transmission. And visa-versa: automatically receives JSON strings and rehydrates the strings into JavaScript objects / arrays.

      var command = {
          client:'sam', 
          points:[{x:5,y:10},...],
          // optionally add styling (strokeStyle, linewidth, etc)
      };
      
      // serialize a command 
      var jsonCommand = JSON.stringify(command);
      
      // deserialize a command
      var command = JSON.parse(jsonCommand);
      
    • Its very important (critical!) to keep all drawings "atomic" -- each path drawing should be complete including styling. Don't start a context.beginPath and emit a series of context.lineTo's over time!

      draw(command.points);
      
      // ALWAYS issue complete drawing commands
      // including styling (if any)
      function draw(points);
          var ptsLength=points.length;
          context.beginPath;
          context.moveTo(points[0].x,points[0].y);
          for(var i=0;i<ptsLength;i++){
              var pt=points[i];
              context.lineTo(pt.x,pt.y);
          }
          context.stroke();
      }
      
    • Don't leave a path open: So don't design a socket app to send partial drawing points (which leaves the drawing operation incomplete). This implies you should wait for a users drag operation to complete before emitting a full drawing operation.

      var isDown=false;
      var commands=[];
      var points;
      var lastX,lastY;
      
      
      // on mousedown ...
      // reinitialize the accumulated points array
      // with the mousedown point
      function handleMouseDown(e){
      
          // tell the browser we're handling this event
          e.preventDefault();
          e.stopPropagation();
      
          // get mouse position
          lastX=parseInt(e.clientX-offsetX);
          lastY=parseInt(e.clientY-offsetY);
      
          // reset the accumulated points array
          // add the point to the accumulated points array
          points=[ {x:lastX, y:lastY} ];          
      
          // set the isDown flag
          isDown=true;
      }
      
      
      // on mousemove ...
      // add the current mouse position to the accumulated points array
      function handleMouseMove(e){
      
          if(!isDown){return;}
      
          // tell the browser we're handling this event
          e.preventDefault();
          e.stopPropagation();
      
          // get mouse position
          mouseX=parseInt(e.clientX-offsetX);
          mouseY=parseInt(e.clientY-offsetY);
      
          // draw the newest local path segment
          // so the local user can see while they're drawing
          context.beginPath();
          context.moveTo(lastX,lastY);
          context.lineTo(mouseX,mouseY);
          context.stroke();
          // save the last x,y
          lastX=mouseX;
          lastY=mouseY;
      
          // add the point to the accumulated points array
          points=[ {x:mouseX, y:mouseY} ];
      }
      
      
      // on mouseup ...
      // end the current draw operation
      // and add the points array to the commands array
      function handleMouseOut(e){
      
          // tell the browser we're handling this event
          e.preventDefault();
          e.stopPropagation();
      
          // clear the isDown flag
          isDown=false;
      
          // add the current set of points 
          // to the accumulated commands array
          commands.push({
              client:myName,
              stroke:myCurrentStrokeColor,
              points:points
          });
      
      }
      
    • Use a separate loop to emit our local drawing commands to the server and to draw incoming remote drawing commands:

      // vars to schedule drawing from remote clients
      // and sending local drawings to server
      var nextDrawingTime, nextSendingTime;
      var drawingTimeDelay=1000; // or some other delay, but don't be a burden!
      var sendingTimeDelay=1000; // or some other delay, but don't be a burden!
      
      // start the processing loop (it runs continuously non-stop)
      requestAnimationFrame(process);
      
      function process(time){
      
          // a simplification ...
          // don't interrupt if the local user is drawing
          if(isDown){ return; }
      
          // draw incoming strokes
          if(time>nextDrawingTime && receivedCommands.length>0){
      
              // set the next drawing time for remote draws
              nextDrawingTime=time+drawingTimeDelay;
      
              // draw all accumulated received commands
              for(var i=0;i<receivedCommands.length;i++){
                  var c=receivedCommands[i];
                  if(c.client!==myName){
                      draw(c.points);
                  }
              }
              receivedCommands.length=0;
      
          // emit outgoing strokes
          } else if(time>nextSendingTime && commands.length>0){
      
              // set the next emitting time for locally drawing paths
              nextSendingTime=time+sendingTimeDelay;
      
              // JSON.stringify
              var jsonPacket=JSON.stringify(commands);
      
              // reset the set of local drawing commands
              commands=[];
      
              // emit to server for broadcast to everyone
      
          }
      
          requestAnimationFrame(process);
      }
      
    • Have the server do some important tasks:

      • Add a timestamp to each broadcast if your choice of websockets library doesn't automatically include a timestamp.

      • Save all received drawing commands (database) because things go wrong and you will probably have to full re-synchronize the clients from time to time.

    • Mousemove fires about 30 times per second so a large quantity of points will be accumulated. To reduce data transmission size, consider using a path reduction algorithm to remove redundant points. One good algorithm is the Douglas Peucker path simplification algorithm.

    There's so much more to a good whiteboard app, but that's all the time I have for now ... Good luck with your project! :-)