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