前言
前段时间比较迷恋【你画我猜】小游戏,于是自己也动手写个一个类似的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)
var app = require('express')();
var http = require('http').createServer(app);
var io = 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)
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io()
</script>
这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。
4、编写【绘画】客户端、服务端代码
- 新配置一个canvas路由
- 新增相应文件:canvas.ejs;canvas.js;canvas.css
- 在服务器(www文件)编写接受与发布绘画数据流逻辑
代码展示区
canvas.ejs
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<link rel='stylesheet' href='/stylesheets/style.css' />
<link href="/stylesheets/canvas.css" rel="stylesheet" type="text/css">
<script type="text/javascript" src="/javascripts/jquery.js"></script>
</head>
<body>
<div class="pro-canvas">
<canvas id="canvas">您的浏览器不支持canvas</canvas>
<div id="controller">
<div id="black_btn" class="color_btn color_btn_selected"></div>
<div id="blue_btn" class="color_btn"></div>
<div id="green_btn" class="color_btn"></div>
<div id="red_btn" class="color_btn"></div>
<div id="orange_btn" class="color_btn"></div>
<div id="yellow_btn" class="color_btn"></div>
<div id="clear_btn" class="op_btn">清除</div>
<div class="clearfix"></div>
</div>
</div>
<img id="drawCanvas" width="300px" height="200px">
</body>
<script type="text/javascript" src="/javascripts/canvas.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
// 连接服务器
var socket = io()
// 绘画客户端与猜图客户端渲染的页面都是一样的,现在根据url中的username的参数值做判断
// 如果当前是username为lsp,则展示canvas绘画区,即为绘画客户端,其他为猜图客户端
var username = window.location.search.split('=')[1]
if (username !== 'lsp') {
$('.pro-canvas').css('display', 'none')
}
socket.on('drawCanvas', function (data) {
// 接收到服务端传的数据流,作为img标签src属性值,生成图片展示
$("#drawCanvas").attr('src', data)
})
</script>
</html>
cancvas.js
//定义宽和高
var canvasWidth = Math.min(800, $(window).width() - 20) // 适配移动端
var canvasHeight = canvasWidth
var strokeColor = 'black' //当前笔的颜色
var isMouseDown = false // 定义鼠标是否按下
var lastLoc = {x: 0, y: 0} //定义上一次鼠标的位置,
var lastTimestamp = 0 //定义时间戳
var lastLineWidth = -1 //定义上一次线条的宽度
var canvas = document.getElementById('canvas') //拿到canvas
var drawCanvas = document.getElementById('drawCanvas') //拿到img
var context = canvas.getContext('2d') // 拿到相应的上下文绘图环境
//设定画布的宽和高
canvas.width = canvasWidth
canvas.height = canvasHeight
//图片与画布展示一致,宽高一致
drawCanvas.width = canvasWidth
drawCanvas.height =canvasHeight
//适配移动端
$('#controller').css('width', canvasWidth + 'px')
// 绘制米字格
drawGrid()
//canvas导出数据流,传值给后台
function returnData() {
// 触发服务端'startConnect'事件,传值给后台
socket.emit('startConnect', canvas.toDataURL())
}
// 轮循
var longPolling
function polling() {
longPolling = setInterval(function() {
returnData() }, 200)
}
// 清除按钮操作
$("#clear_btn").click(
function(e) {
context.clearRect(0, 0, 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')
}
)
//逻辑整合
function beginStroke(point) {
isMouseDown = true
lastLoc = windowToCanvas(point.x, point.y )
lastTimestamp = new Date().getTime()
polling()
}
function endStroke() {
isMouseDown = false
clearInterval(longPolling) // 清除轮询
}
// 绘画
function moveStroke(point){
//核心代码
var curLoc = windowToCanvas(point.x, point.y )
var curTimestamp = new Date().getTime()
/****Draw Start****/
context.beginPath()
context.moveTo(lastLoc.x, lastLoc.y)
context.lineTo(curLoc.x, curLoc.y)
//计算速度
var s = calcDistance(curLoc, lastLoc)
var t = curTimestamp - lastTimestamp
var lineWidth = 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({x: e.clientX, y: e.clientY}) //可以绘图了
}
}
//触控事件,移动端
canvas.addEventListener('touchstart', function(e){
e.preventDefault()
touch = e.touches[0]
beginStroke({ x: touch.pageX, y: touch.pageY })
})
canvas.addEventListener('touchmove', function(e){
e.preventDefault()
if (isMouseDown) { // 确定鼠标按下
touch = e.touches[0]
moveStroke({ x: touch.pageX, y: touch.pageY }) //可以绘图了
}
})
canvas.addEventListener('touchend', function(e){
e.preventDefault()
endStroke()
})
/**
* 计算笔的宽度
*/
function calcLineWidth(t, s) {
var v = s / t
var resultLineWidth = 0
if ( v <= 0.1 ) {
resultLineWidth = 10
} else if ( v >= 10 ) {
resultLineWidth = 1
} else {
resultLineWidth = 10 - (v - 0.1) / (10 - 0.1) * (10 - 1)
}
if (lastLineWidth == -1) {
return resultLineWidth
} else {
return lastLineWidth * 2/3 + resultLineWidth * 1/3
}
}
/**
* 计算距离
*/
function calcDistance(loc1, loc2) {
return Math.sqrt((loc1.x - loc2.x)*(loc1.x - loc2.x) + (loc1.y - loc2.y)*(loc1.y - loc2.y))
}
/**
* 窗口到画布的位置
*/
function windowToCanvas(x, y) {
var box = canvas.getBoundingClientRect()
return {x: Math.round(x-box.left), y: Math.round(y-box.top)}
}
/** 绘制米字格 **/
function drawGrid() {
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(0, 0)
context.lineTo(canvasWidth, canvasHeight)
context.moveTo(canvasWidth, 0)
context.lineTo(0, canvasHeight)
context.moveTo(canvasWidth / 2, 0)
context.lineTo(canvasWidth / 2, canvasHeight)
context.moveTo(0, canvasHeight / 2)
context.lineTo(canvasWidth, canvasHeight / 2 )
context.closePath()
context.lineWidth = 1
context.stroke()
context.restore()
}
服务端代码(www)
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('drawguess:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
var io = 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'事件,返回数据流
io.sockets.emit('drawCanvas', data)
})
});
/**
* 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.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === '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:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
页面展示效果
githunb地址: https://github.com/Liao640/guessCavas
项目部署地址: http://193.112.106.197:8085/
用户名为lsp为绘画客户端,其他用户登陆为猜图客户端。功能后续更新