Rishabh Jain
Rishabh Jain

Reputation: 536

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

Upvotes: 1

Views: 815

Answers (2)

markE
markE

Reputation: 105015

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! :-)

Upvotes: 3

Prusse
Prusse

Reputation: 4315

You will need to keep track of each client's "last point" and prior to issuing context.lineTo(mouse.x,mouse.y) do a moveTo to the client's "last point" (that will also apply to the color so you can set the correct client's color).

To get an ideia you can try to do:

  • on your mousedown handler (socketEngage) to do (plus the original code there)

    mouse.last_x = e.offsetX; mouse.last_y = e.offsetY;

(beginning of the function)

  • in your mousemove handler (socketPutPoint)

    mouse.last_x = mouse.x; mouse.last_y = mouse.y;

(beginning of the function)

  • and on putPoint before context.lineTo(mouse.x,mouse.y) add

    if (mouse.last_x && mouse.last_y) context.moveTo(mouse.last_x,mouse.last_y);

Hope that you could do the rest of adjustments.

Upvotes: 1

Related Questions