1. COBO
Aplicatie Web ce permite editarea
colaborativa a unei planse (whiteboard)
folosind facilitatile oferite de canvas
Coscotin Vasilica
Abstract. Cobo este o aplicatie web ce permite editarea colaborativa a unei planse
folosind facilitatile oferite de elementul Canvas din HTML5. Comunicarea intre clienti
(browsere) se face folosind Socket.IO. Aplicatia ofera utilizatorului posibilitatea de a desena cu
diferite “creioane”, de a insera figuri geometrice , poze si text. Plansa poate fi salvata la nivel local si
restaurat ulterior sau exportata in format PNG.
1 Introducere
Cobo a fost creata in cadrul laboratorului de Aplicatii Web la Nivel de Client de la Facultatea
de Informatica Iasi sustinut de Ciprian Amariei, profesor de curs fiind Dr. Sabin-Corneliu Buraga
.Aplicatia este Open Source sursa putand fi descarcata la adresa
http://students.info.uaic.ro/~vasilica.coscotin/cobo.
2 Tehnologii folosite
Cobo a fost creata folosind HTML5 , Javascript si Socket.IO. Elementul canvas din HTML5 a
permis redare in mod dinamic a scripturilor de redare a formelor 2D si a imaginilor de tip Bitmap. Am
folosit libraria jQuery ,pentru Javascript , pentru a realiza medodele de intercatiunde dintre utilizator si
browser . Pentru a permite comunicare in timp real intre clienti (browsere) am folosit Socket.IO care
este o implementare a interfetei HTTP Socket.
2. 2.1 Canvas si Javascript
2.1.1 Functii pentru desenare
Fiecare canvas are un context de desenare in aplicatia COBO este unul 2D si este apelat in felul
urmator :
var canvas = document.getElementById("sheet");
var context = canvas.getContext("2d");
unde sheet este ID-ul elementului canvas din index.html.
Pentru a desena forme geometrice in contextul context folosim urmatoarele functii :
pentru a desena linii (unde x1,y1 sunt coordonatele mouse-ului cand are loc evenimetnul
mouseDown iar x2,y2 sunt coordonatele mouse-ului cand are loc evenimetnul mouseUp)
var line = function(x1, y1, x2, y2, s) {
s.globalCompositeOperation = 'source-over';
setStyle(s);
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.closePath();
context.stroke();
}
pentru a desena cu un creion (unde x1,y1 sunt coordonatele mouse-ului cand are loc
evenimetnul mouseDown iar x2,y2 sunt coordonatele mouse-ului cand are loc evenimetnul
mouseMove):
var pencil = function(x1, y1, x2, y2, s) {
s.globalCompositeOperation = 'source-over';
setStyle(s);
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.closePath();
context.stroke();
}
pentru a desena un cerc (unde x1,y1 sunt coordonatele mouse-ului cand are loc evenimetnul
mouseDown iar x2,y2 sunt coordonatele mouse-ului cand are loc evenimetnul mouseUp)
var circle = function(x1, y1, x2, y2, s) {
s.globalCompositeOperation = 'source-over';
minX = (x1<x2) ? x1 : x2;
minY = (y1<y2) ? y1 : y2;
radiusX = Math.abs(x1 - x2) / 2;
radiusY = Math.abs(y1 - y2) / 2;
context.beginPath();
setStyle(s);
context.arc(minX+radiusX, minY+radiusY, radiusX, 0, 2*Math.PI, false);
context.closePath(); context.stroke(); }
pentru a desena un cerc (unde x1,y1 sunt coordonatele mouse-ului cand are loc evenimetnul
3. mouseDown iar x2,y2 sunt coordonatele mouse-ului cand are loc evenimetnul mouseUp)
var rectangle = function(x1, y1, x2, y2, s) {
s.globalCompositeOperation = 'source-over';
minX = (x1<x2) ? x1 : x2;
minY = (y1<y2) ? y1 : y2;
xl = Math.abs(x1-x2);
yl = Math.abs(y1-y2);
setStyle(s);
context.beginPath();
context.rect(minX, minY, xl, yl);
context.closePath();
context.stroke();
}
In toate cele 4 functii parametrul s reprezinta stilul formei (culoare, grosime,transparenta,
lineJoin si lineCap).
2.1.2 Mouse handlers
Pentru a controla evenimentele mouse-ului am folosit urmatoarele functii:
// event handler pentru Mouse Down
$('#sheet').mousedown(function(e) {
mouseDown = true; // pentru a verifica in mouseMove daca este tinut apasat clik
stanga
mouseX = e.pageX - this.offsetLeft; // pentru a lua pozitia mouseului fata de
canvas
mouseY = e.pageY - this.offsetTop;
context.moveTo(mouseX, mouseY);
switch(tool){
case Pen: // desenam in cazul in care unelata selectata este Pen
pencil(mouseX, mouseY, mouseX+1, mouseY, style);
msg = { "x1":mouseX, "y1":mouseY, "x2":mouseX+1, "y2":mouseY,
"tool":Pen, "style":style };
socket.send(JSON.stringify(msg));
break;
case Text:
if($('#textVal').val() != '')
{ //vreificam daca sunt bifate optiunile pentru bold
,italic si underline
font = '';
if($('#bold').is(':checked'))
font += 'bold ';
if($('#underline').is(':checked'))
font += 'underline ';
if($('#italic').is(':checked'))
font += 'italic ';
font += $('#penSize').slider('value')*5 + 'px sans-serif';
context.font = font;
context.fillStyle = style.strokeStyle;
context.fillText($('#textVal').val(), mouseX, mouseY);
msg = { "x":mouseX, "y":mouseY, "tool":Text, "font":font,
"text":$('#textVal').val(), "style":style };
socket.send(JSON.stringify(msg));// trimitem la ceilalti
clienti prin server
}
break;
case IMAGE: // pentru a desa o imagine pe canvas
context.drawImage(image, mouseX, mouseY);
msg = { "x":mouseX, "y":mouseY, "image":image.src, "tool":Img };
socket.send(JSON.stringify(msg));
break;
break;
}
4. });
// event handler pentru Mouse Up
$('#sheet').mouseup(function(e) {
mouseDown = false; // nu mai este tinut apasat click-ul
newX = e.pageX - this.offsetLeft;// pentru a lua pozitia mouseului fata de canvas
newY = e.pageY - this.offsetTop;// pentru a lua pozitia mouseului fata de canvas
oldX = mouseX;
oldY = mouseY;
switch(tool) {
case Pen: // nu facem nimic pentru ca desenarea s-a facut in cadrul mouseMove
break;
case Circ: // desenam cecul din pozitia oldX oldY in pozitia newX newY si cu
diametrul = distanta dintre oldX newX respectiv oldY newY
circle(oldX, oldY, newX, newY, style);
msg = { "x1":oldX, "y1":oldY, "x2":newX, "y2":newY, "tool":Circ,
"style":style };
socket.send(JSON.stringify(msg));
break;
case Rec:// desenam patratul din pozitia oldX oldY in pozitia newX newY si cu
diametrul = distanta dintre oldX newX respectiv oldY newY
rectangle(oldX, oldY, newX, newY, style);
msg = { "x1":oldX, "y1":oldY, "x2":newX, "y2":newY, "tool":Rec,
"style":style };
socket.send(JSON.stringify(msg));
break;
case Line:// desenam linia din pozitia oldX oldY in pozitia newX newY si cu
diametrul = distanta dintre oldX newX respectiv oldY newY
line(oldX, oldY, newX, newY, style);
msg = { "x1":oldX, "y1":oldY, "x2":newX, "y2":newY, "tool":Line,
"style":style };
socket.send(JSON.stringify(msg));
break;
}
});
// event handler pentru mouse move
$('#sheet').mousemove(function(e) {
newX = e.pageX - this.offsetLeft;
newY = e.pageY - this.offsetTop;
if(mouseDown) { //cat timp e tinut apasat butonul de mouse deseneaza sau sterge
oldX = mouseX;
oldY = mouseY;
switch(tool){
case Pen:
pencil(oldX, oldY, newX, newY, style);
msg = { "x1":oldX, "y1":oldY, "x2":newX,
"y2":newY,"tool":Pen,"style":style };
socket.send(JSON.stringify(msg));
mouseX = newX;
mouseY = newY;
break;
case Eraser:
eraser(oldX, oldY, newX, newY, style);
msg = { "x1":oldX, "y1":oldY, "x2":newX, "y2":newY,
"tool":Eraser, "style":style };
socket.send(JSON.stringify(msg));
mouseX = newX;
mouseY = newY;
break;
break;
}
}
});
// cand mouse-ul paraseste suprafata canvasului
$('#sheet').mouseleave(function() {
5. mouseDown = false;
});
Aceste evenimente apar la actiunile mouse-ului pe suprafata browser-ului.
2.1.3 Alegerea culorii
Alegerea culorii se face prin alegerea cantitatilor de rosu, albastru si verde prin miscarea la
stanga sau dreapta a slider-elor
// color and size
//regleaza in fuctie de slider-e
colaore(cantitatea de Red, Green si Blue)
$('#red').slider({
value: 95,
min: 0,
max: 255,
slide: function(event, ui) {
changeColor();
}
}).width(160);
$('#green').slider({
value: 221,
min: 0,
max: 255,
slide: function(event, ui) {
changeColor();
}
}).width(160);
$('#blue').slider({
value: 122,
min: 0,
max: 255,
slide: function(event, ui) {
changeColor();
}
}).width(160);
$('#opac').slider({
value: 100,
min: 0,
max: 100,
slide: function(event, ui) {
changeColor();
}
}).width(160);
//Schimb culoarea pensulei actualizand style- ul si in acleasi timp schimb
si culoarea fundalului din div-ul Selected color
var changeColor = function(){
c = 'rgba(' + $('#red').slider('value') + ',' + $('#green').slider('value') +
',' + $('#blue').slider('value') + ',' + ($('#opac').slider('value') / 100) + ')';
$('#selectedColor').css({backgroundColor: c});
style.strokeStyle = 'rgb(' + $('#red').slider('value') + ',' + $
('#green').slider('value') + ',' + $('#blue').slider('value') + ')';
6. style.globalAlpha = ($('#opac').slider('value') / 100);
}
2.1.3 Dimensiune pensulei
Dimensiune este setata tot cu ajutorul unui slider
$('#penSize').slider({
value: 2,
step: 0.1,
min: 0.5,
max: 20,
slide: function(event, ui) {
style.lineWidth = ui.value;
}
}).width(160);
2.1.4 Actiuni pe fisier
Dupa cum am precizat si in abstract aplicatia ofera utilizatului posibiliatatea de a salva desenul
la nivel local , de a restaura desenul salvat si de a exporta desenul in format PNG. Aceste actiuni sunt
realizate in cu jQuery :
// file functions
//salveaza imaginea canvas la nivel-ul browser-ului
var saveFile = function(){
localStorage.setItem("sheet", canvas.toDataURL());
}
$('#save').click(function() {
saveFile();
});
//deseneaza imaginea incarcata din "baza de date" a browser-ului
var load = function(imageSource) {
image = new Image();
image.src = imageSource;
canvas.width = canvas.width;
context.drawImage(image, 0, 0);
}
//incarca imaginea din "baza de date" a browserului
$('#load').click(function() {
imageSource = localStorage.getItem("sheet");
load(imageSource);
msg = { "tool":Load, "image":imageSource };
socket.send(JSON.stringify(msg));
});
//face o coala noua stergat tot ce este in canvas
$('#newSheet').click(function(){
canvas.width = canvas.width;
msg = { "tool":New };
socket.send(JSON.stringify(msg));
});
//incarca o imagine selectata din dialog de selecare a unui fisier
$('#imageSource').change(function() {
var file = document.getElementById('imageSource').files[0];
reader = new FileReader();
7. reader.onload = function (event) {
image = new Image();
image.src = event.target.result;
};
reader.readAsDataURL(file);
tool = IMAGE;
});
//exporta imagina in fomrat Png
$('#export').click(function() {
window.open(canvas.toDataURL());
});
2.2 Transmiterea datelor
Transmiterea datelor se cu ajutorul Socket.IO care in functie de browser selecteaza un protocol
de comunicare WebSocket pentru Google Chrome, xhr-multipart pentru Mozilla.
Functiile pentru trasferul datelor sunt urmatoarele:
//realoziaza conexiunea la server
var socket = new io.Socket('localhost',{
port: 9125
});
socket.connect();
//trimite date la ceilalti clienti in functie de toolul selectat
socket.on('message', function(msgData) {
msgData = $.parseJSON(msgData);
switch(msgData.tool){
case Pen:
pencil(msgData.x1, msgData.y1, msgData.x2, msgData.y2,
msgData.style);
break;
case Circ:
circle(msgData.x1, msgData.y1, msgData.x2, msgData.y2,
msgData.style);
break;
case Rec:
rectangle(msgData.x1, msgData.y1, msgData.x2, msgData.y2,
msgData.style);
break;
case Text:
context.font = msgData.font;
context.fillStyle = msgData.style.strokeStyle;
context.fillText(msgData.text, msgData.x, msgData.y);
break;
case Load:
load(msgData.image);
break;
case New:
canvas.width = canvas.width;
break;
case IMAGE:
image = new Image();
image.src = msgData.image;
context.drawImage(image, msgData.x, msgData.y);
break;
8. case Eraser:
eraser(msgData.x1, msgData.y1, msgData.x2, msgData.y2,
msgData.style);
break;
break;}
}); break;
case Line:
line(msgData.x1, msgData.y1, msgData.x2, msgData.y2,
msgData.style);
break;
case Text:
context.font = msgData.font;
context.fillStyle = msgData.style.strokeStyle;
context.fillText(msgData.text, msgData.x, msgData.y);
break;
case Load:
load(msgData.image);
break;
case New:
canvas.width = canvas.width;
break;
case IMAGE:
image = new Image();
image.src = msgData.image;
context.drawImage(image, msgData.x, msgData.y);
break;
case Eraser:
eraser(msgData.x1, msgData.y1, msgData.x2, msgData.y2,
msgData.style);
break;
break;}
});
9. 3 Modalitate de utilizare
Din meniul din dreapta sus se alege pensula pentru desenare posibilitatile fii :
Pencil, Line, Recangle, Circle, Eraser dupa care se deseneaza pe canvas ( foaia alba ).
Meniul din dreapta se poate alege culoare pensulei precum si dimensiunea acestia (la optiunea
size). Tot aici la submeniu text putem scrie textul ce urmeaza sa fie inserat si selecta stilul textului
(Bold,Italic,Underline)
Din partea de sus meniului putem selecta daca sa salvam imaginea la nivel local , sa reincarcam
imaginea salvata si sa o exportam in format PNG intr-o alta fereastra.
4 Bibliografie
http://socket.io/
http://www.w3schools.com/
http://docs.jquery.com/Main_Page
http://profs.info.uaic.ro/~busaco/teach/courses/cliw/web.html