Socket.io+canvas實現實時繪畫
前言
前段時間比較迷戀【你畫我猜】小遊戲,於是自己也動手寫個一個類似的demo。【你畫我猜】原理就是藉助socket.io技術實現同步繪畫。
WebSocket與Socket.io介紹
WebSocket
WebSocket是HTML5一種新通信協議。它實現了瀏覽器與伺服器之間的雙向通信。瀏覽器通過javaScript向伺服器發出建立WebSocket連接的請求,連接建立以後,客戶端和服務端就可以通過TCP連接直接交換數據。
Socket.io
實際上Socket.io與websocket並不完全等同。它完全由JavaScript實現、基於Node.js、支持WebSocket協議用於實時通信、跨平台的開源框架,它包括了客戶端的JavaScript和伺服器端的Node.js。
Socket.io是將Websocket和輪詢(Polling)機制以及其它的實時通信方式封裝成了通用的介面,並且在服務端實現了這些實時通信機制。Websocket僅僅是Socket.io實現實時通信的一個子集。
【同步繪畫】整理架構
整體框架非常簡單,需要一台伺服器和多台客戶端
繪圖客戶端:進行canvas繪圖,把繪畫生成的base64數據流,傳給伺服器。
WebSocket伺服器:接收到的數據流,又將數據分發到指定的客戶端。
猜圖客戶端:接收到伺服器傳的base64數據流,作為img標籤src屬性值,生成圖片。
創建項目
1、創建express項目(已確保你安裝了node/express)
$ express --view=ejs項目名
$ npm install
$ cd項目名
$ node ./bin/www
運行成功後,在瀏覽器中打開http://localhost:3000
默認埠號為3000,可修改)展示效果如下:
2、安裝socket.io依賴包
Socket.IO由兩部分組成:
一個服務端用於集成(或掛載) 到Node.JS HTTP 伺服器: socket.io
一個載入到瀏覽器中的客戶端:socket.io-client
開發環境下,socket.io會自動提供客戶端,只需要安裝一個模塊:
$ npm socket.io
3、服務端引入socket.io,客戶端引入socket.io-client
服務端(app.js/www)
varapp =require("express")();
varhttp =require("http").createServer(app);
vario =require("socket.io")(http);
app.get("/",function(req, res){
res.sendFile(__dirname +"/index.html");
});
io.on("connection",function(socket){
console.log("a user connected");
});
http.listen(3000,function(){
console.log("listening on *:3000");
})
這段代碼作用如下:
Express初始化app作為HTTP伺服器的回調函數。
我們通過傳入http(HTTP伺服器)對象初始化了socket.io的一個實例。
定義了一個路由/來處理首頁訪問。
然後監聽connection事件來接收sockets,並將連接信息列印到控制台。
使http伺服器監聽埠3000。
客戶端(index.ejs)
var socket = io()
這樣就載入了socket.io-client。socket.io-client暴露了一個io全局變數,然後連接伺服器。
4、編寫【繪畫】客戶端、服務端代碼
新配置一個canvas路由
新增相應文件:canvas.ejs;canvas.js;canvas.css
在伺服器(www文件)編寫接受與發布繪畫數據流邏輯
代碼展示區
canvas.ejs
您的瀏覽器不支持canvas
清除
//連接伺服器
var socket = io()
//繪畫客戶端與猜圖客戶端渲染的頁面都是一樣的,現在根據url中的username的參數值做判斷
//如果當前是username為lsp,則展示canvas繪畫區,即為繪畫客戶端,其他為猜圖客戶端
if (username !== "lsp") {
$(".pro-canvas").css("display", "none")
}
socket.on("drawCanvas", function (data) {
//接收到服務端傳的數據流,作為img標籤src屬性值,生成圖片展示
$("#drawCanvas").attr("src", data)
})
cancvas.js
//定義寬和高
varcanvasWidth =Math.min(800, $(window).width() -20)//適配移動端
varcanvasHeight = canvasWidth
varstrokeColor ="black"//當前筆的顏色
varisMouseDown =false//定義滑鼠是否按下
varlastLoc =//定義上一次滑鼠的位置,
varlastTimestamp =//定義時間戳
varlastLineWidth =-1//定義上一次線條的寬度
varcanvas =document.getElementById("canvas")//拿到canvas
vardrawCanvas =document.getElementById("drawCanvas")//拿到img
varcontext = canvas.getContext("2d")//拿到相應的上下文繪圖環境
//設定畫布的寬和高
canvas.width = canvasWidth
canvas.height = canvasHeight
//圖片與畫布展示一致,寬高一致
drawCanvas.width = canvasWidth
drawCanvas.height =canvasHeight
//適配移動端
$("#controller").css("width", canvasWidth +"px")
//繪製米字格
drawGrid()
//canvas導出數據流,傳值給後台
functionreturnData() {
//觸發服務端"startConnect"事件,傳值給後台
socket.emit("startConnect", canvas.toDataURL())
}
//輪循
varlongPolling
functionpolling() {
longPolling = setInterval(function() {
returnData() },200)
}
//清除按鈕操作
$("#clear_btn").click(
function(e) {
context.clearRect(,, canvasWidth, canvasHeight)
drawGrid()//重新繪製米字格
returnData()//發送數據流給伺服器
}
)
//選擇繪畫顏色
$(".color_btn").click(
function(e){
$(".color_btn").removeClass("color_btn_selected")
$(this).addClass("color_btn_selected")
strokeColor = $(this).css("background-color")
}
)
//邏輯整合
functionbeginStroke(point) {
isMouseDown =true
lastLoc = windowToCanvas(point.x, point.y )
lastTimestamp =newDate().getTime()
polling()
}
functionendStroke() {
isMouseDown =false
clearInterval(longPolling)//清除輪詢
}
//繪畫
functionmoveStroke(point){
//核心代碼
varcurLoc = windowToCanvas(point.x, point.y )
varcurTimestamp =newDate().getTime()
/****Draw Start****/
context.beginPath()
context.moveTo(lastLoc.x, lastLoc.y)
context.lineTo(curLoc.x, curLoc.y)
//計算速度
vars = calcDistance(curLoc, lastLoc)
vart = curTimestamp - lastTimestamp
varlineWidth = calcLineWidth( t, s )
context.strokeStyle = strokeColor
context.lineWidth = lineWidth
context.lineCap ="round"
context.lineJoin ="round"
context.stroke()
/****Draw End****/
lastLoc = curLoc
lastTimestamp = curTimestamp
lastLineWidth = lineWidth
}
//滑鼠事件,web端
canvas.onmousedown =function(e){
e.preventDefault()//阻止默認的動作發生
beginStroke({ x: e.clientX, y: e.clientY })
}
canvas.onmouseup =function(e){
e.preventDefault()
endStroke()
}
canvas.onmouseout =function(e){
e.preventDefault()
endStroke()
}
canvas.onmousemove =function(e){
if(isMouseDown) {//確定滑鼠按下
e.preventDefault()
moveStroke()//可以繪圖了
}
}
//觸控事件,移動端
canvas.addEventListener("touchstart",function(e){
e.preventDefault()
touch = e.touches[]
beginStroke({ x: touch.pageX, y: touch.pageY })
})
canvas.addEventListener("touchmove",function(e){
e.preventDefault()
if(isMouseDown) {//確定滑鼠按下
touch = e.touches[]
moveStroke({ x: touch.pageX, y: touch.pageY })//可以繪圖了
}
})
canvas.addEventListener("touchend",function(e){
e.preventDefault()
endStroke()
})
/**
*計算筆的寬度
*/
functioncalcLineWidth(t, s) {
varv = s / t
varresultLineWidth =
if( v
resultLineWidth =10
}elseif( v >=10) {
resultLineWidth =1
}else{
resultLineWidth =10- (v -0.1) / (10-0.1) * (10-1)
}
if(lastLineWidth ==-1) {
returnresultLineWidth
}else{
returnlastLineWidth *2/3+ resultLineWidth *1/3
}
}
/**
*計算距離
*/
functioncalcDistance(loc1, loc2) {
returnMath.sqrt((loc1.x - loc2.x)*(loc1.x - loc2.x) + (loc1.y - loc2.y)*(loc1.y - loc2.y))
}
/**
*窗口到畫布的位置
*/
functionwindowToCanvas(x, y) {
varbox = canvas.getBoundingClientRect()
return
}
/**繪製米字格**/
functiondrawGrid() {
context.save()
//繪製紅色的正方形邊框
context.strokeStyle ="rgb(230, 11, 9)"
context.beginPath()
context.moveTo(3,3)
context.lineTo(canvasWidth -3,3)
context.lineTo(canvasWidth -3, canvasHeight -3)
context.lineTo(3, canvasHeight -3)
context.closePath()
context.lineWidth =6
context.stroke()
//繪製米字格
context.beginPath()
context.moveTo(,)
context.lineTo(canvasWidth, canvasHeight)
context.moveTo(canvasWidth,)
context.lineTo(, canvasHeight)
context.moveTo(canvasWidth /2,)
context.lineTo(canvasWidth /2, canvasHeight)
context.moveTo(, canvasHeight /2)
context.lineTo(canvasWidth, canvasHeight /2)
context.closePath()
context.lineWidth =1
context.stroke()
context.restore()
}
服務端代碼(www)
#!/usr/bin/env node
/**
* Module dependencies.
*/
varapp =require("../app");
vardebug =require("debug")("drawguess:server");
varhttp =require("http");
/**
* Get port from environment and store in Express.
*/
varport = normalizePort(process.env.PORT ||"3000");
app.set("port", port);
/**
* Create HTTP server.
*/
varserver = http.createServer(app);
vario =require("socket.io")(server)
io.on("connection",function(socket) {
console.log("a user connected");
//監聽"startConnect"事件,接受數據流
socket.on("startConnect",function(data) {
// console.log("startConnect", data)
//向客戶端廣播"drawCanvas"事件,返回數據流
})
});
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on("error", onError);
server.on("listening", onListening);
/**
* Normalize a port into a number, string, or false.
*/
functionnormalizePort(val) {
varport =parseInt(val,10);
if(isNaN(port)) {
// named pipe
returnval;
}
if(port >=) {
// port number
returnport;
}
returnfalse;
}
/**
* Event listener for HTTP server "error" event.
*/
functiononError(error) {
if(error.syscall !=="listen") {
throwerror;
}
varbind =typeofport ==="string"
?"Pipe "+ port
:"Port "+ port;
// handle specific listen errors with friendly messages
switch(error.code) {
case"EACCES":
console.error(bind +" requires elevated privileges");
process.exit(1);
break;
case"EADDRINUSE":
console.error(bind +" is already in use");
process.exit(1);
break;
default:
throwerror;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
functiononListening() {
varaddr = server.address();
varbind =typeofaddr ==="string"
?"pipe "+ addr
:"port "+ addr.port;
debug("Listening on "+ bind);
}
頁面展示效果
githunb地址:https://github.com/Liao640/guessCavas
項目部署地址:http://193.112.106.197:8085/
用戶名為lsp為繪畫客戶端,其他用戶登陸為猜圖客戶端。功能後續更新
TAG:興海開放平台 |