1

Intro:

I am building a website which one of its main feature is a whiteboard. You can open a session with another user and then whatever you paint on the board he will see and vice versa.

implementated using HTML and javascript, peer to peer.

The issue:

Right now two things can trigger a drawing on your board - you draw something or your partner in the session draws something. (both trigger a drawing event).

The problems begin when you draw something and in the same time your partner does too. (drawing is mixed, my input and the one received by my partner are mixed, can not parallelly deal with two drawings input)

The solution: (?)

Multithreading could have solved it. Dealing with the partner's drawing in a separate thread. However, JavaScript is not multithreaded. I read about the options of using 'worker'(HTML 5) but if i understand correctly it is very limited in working with variables from the main thread and cannot influence the HTML (change what is seen on the screen).

Any ideas?

mosh
  • 404
  • 2
  • 8
  • 16

3 Answers3

1

Multi-threading won't solve this for you. Web Workers are as mentioned already, limited and does not add any benefits in this context as that would require access to DOM etc. (you could use them to process pixels using transferable objects, ie. typed arrays, but you would have to pipe them back to your main JS thread after processing so you would be just as far).

Good news though, you can draw two different paintings at the same time. I assume you use canvas to achieve this.

Simply create two overlaying canvases and feed your local mouse/touch movements to one of the canvases while feeding the data over your web socket to the other canvas.

The other way is to draw each segment in an atomic manner - for each mouse move and for each data received over web socket representing a movement (you may need to iterate here), for example:

ctx.beginPath();                /// reset path for this segment
ctx.moveTo(oldX, oldY);         /// of current client/user
ctx.lineTo(x, y);               /// current position
ctx.strokeStyle = currentColor; /// the one drawing this line
ctx.stroke();

(oldX/Y would be initialized the first time by the mouse down event).

Doing this in a segmented way allows both to draw at the same time, albeit it's a bit more performance hungry. Personally I would go for a layered canvas solution if order of lines isn't important. As your solution would use events it would be asynchronous and would deal with an event queue which should work fine here.

Simply set up some arrays to hold the data (old position, styles etc.) for each user so you can use an ID/index for the current user/client.

Some pseudo code to give a simple overview for segmented approach:

var oldX = [],
    oldY = [],
    currentColor = [], ...;

...set up clients, arrays, mousedown/up etc.

canvas.onmousemove = function(e) {
    var pos = getMousePos ...
    if (isDrawing) drawSegment(0, pos.x, pos.y);  /// f.ex. client 0 = local
}

function handleSocketData() {
    ... get x/y from data stream
    drawSegment(1, x, y);          /// f.ex. client 1 = web socket
}

function drawSegment(client, x, y) {
    ctx.beginPath();
    ctx.moveTo(oldX[client], oldY[client]);
    ctx.lineTo(x, y);
    ctx.strokeStyle = currentColor[client];
    ctx.stroke();

    oldX[client] = x;
    oldY[client] = y;
}

This is of course simplified. You would need to store each point in a point array with the same state details as when drawn to be able to redraw the canvas if cleared, handle multiple segments on the socket and so forth but I think it gives an impression of how to implement the core principle of this method.

Hope this helps!

  • Indeed, it seems like the (unsynchronized) shared state on the canvas is the *real* problem here. Either you separate those states (with multiple canvases), or you remove the state (with atomic operations per segment). – Mattias Buelens Jan 30 '14 at 08:59
  • I'm not sure how the layered canvas would help - although it *is* an interesting approach that may be useful for other tasks, such as "toggling layers". (That is, what difference does it make between switching canvases and colors of lines? In both cases there is no state unless the line start is not appropriately reset per user.) – user2864740 Jan 30 '14 at 08:59
  • @user2864740 it helps in the way that you can draw continuous lines for example which is not possible with a shared canvas as there is only a single path which would have all (f.ex) lineTo's accumulated resulting in a criss-crossed drawing between the data, no color separation and so forth. Hence either segmented approach which resets the path for each line segment or a separate (really *path* rather than canvas per se) solution. –  Jan 30 '14 at 09:03
  • @user2864740 If each actor is assigned a separate canvas, they can store state in that canvas (such as pen position, current color and path) without conflicting with another actor. – Mattias Buelens Jan 30 '14 at 09:03
  • @Ken Good point about keeping the same path going - although I don't know if there is an advantage to do such with a Canvas [which must be rasterized for the path to show up] over something like SVG. The state maintenance (and restoring with moveTo) is trivial for each player. – user2864740 Jan 30 '14 at 09:06
  • @user2864740 I never said the canvas should be controlled by a worker! The whole thing is still single-threaded, but the event handlers operate on separate canvases, thus keeping the canvas state separate. – Mattias Buelens Jan 30 '14 at 09:06
  • @MattiasBuelens Then why even bother with multiple canvases? (Also, I think that `var players = {}` is probably easier to "store state" than Web Workers, unless, perhaps, Web Workers are *already* being used.) – user2864740 Jan 30 '14 at 09:06
  • I see a problem though: if actor B wants to draw on top of something drawn by actor A, his drawing may end up *below* that of actor A because B's layer is *below* A's layer. You don't really have a shared whiteboard anymore, just a bunch of separate whiteboards stacked on top of each other... – Mattias Buelens Jan 30 '14 at 09:07
  • @MattiasBuelens it's a valid point for the layering if important. With the segmented approach this won't be a problem. The last drawing will end up on the top. –  Jan 30 '14 at 09:08
  • @user2864740 SVG is good too (and is rasterized as well). I made an assumption about canvas, but the same principle would apply to both in regards to separation of the data. –  Jan 30 '14 at 09:11
  • @user2864740 The two actors are using the same canvas and retain *state* on that single canvas (such as where the current working path ends). Since another actor may override that state when working on the same canvas, you get conflicts. By using separate canvases, you separate the states as well. However, I agree that an application-defined state (such as `playerState[actor] = {...}`) is much more suitable and would not have the layering problem. – Mattias Buelens Jan 30 '14 at 09:13
  • @Ken , by the way why Multi-threading won't solve it? two process that draw on the screen? – mosh Feb 02 '14 at 07:57
  • @mosh mostly because the main thread will have to wait for the web worker to finish before it could put the resulting data onto the canvas. I other words, you could just as well do the processing on the main thread instead. Although, a web worker could make the UX more responsive so for that reason... –  Feb 02 '14 at 08:07
0

In this case the "race condition" is not related to or solved by threading - it would actually just make it more complicated to have threads.

Instead, the serialization schedule/order needs to be defined and followed when implementing which lines are drawn, and when - e.g. Who's lines go "on top"? And is it by-segment or by-path? Priority by pen-down (first) or pen-up (last)? This can be just as simply handled within context of the asynchronous model used by JavaScript.

The easiest approach is just to draw the lines based on the order in which they are received by a client; if there needs to be a consistent view then the algorithm/protocol needs to introduce synchronization such as a shared counter or "timestamp", perhaps injected by the server when it receives the updates.

The state for the different peers can be trivially maintained in objects (e.g. maps keyed to the peer) and is not hard to setup or maintain.


Web Workers are the closest thing that [browser] JavaScript has to threading.

Web Workers are more akin to BackgroundWorkers as are often used from UI frameworks such as Swing in Java or .NET WebForms. That is, the background task - be it a Web Worker or a Background Worker - is not allowed to modify the UI/HTML but must trigger some form of callback which is then processed on the "correct thread".

This actually works out much better in many cases than unrestricted threading and cross-thread access: no consistency or atomicity worries!

user2864740
  • 60,010
  • 15
  • 145
  • 220
  • Hmm.. serialization means givining up the dream of two things being simultaneously drawn on the screen and instead using a queue. – mosh Jan 30 '14 at 08:42
  • @mosh If you draw N things (say, lines) in the same asynchronous event callback then they will be visible on the *same* UI update. However, you must decide *which* lines are drawn, and it what order - *that* is the serialization order/schedule. Using threads does not address that concept at all (and using threads would only makes it harder as then synchronization must be added without removing the need to define a serialization order for predictable results). – user2864740 Jan 30 '14 at 08:51
0

I don't think multiple threads are required in this case. You can do all your drawing in single thread. Lets say you have a function which draws a line between two points. You can call this function on a local draw event (when user himself draws) and also when partner data is received. You can even animate partner drawing after a short delay.

umair
  • 957
  • 1
  • 9
  • 21