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实现实时通信的一个子集

【同步绘画】整理架构


整体框架非常简单,需要一台服务器和多台客户端 image

  • 绘图客户端:进行canvas绘图,把绘画生成的base64数据流,传给服务器。
  • WebSocket服务器:接收到的数据流,又将数据分发到指定的客户端。
  • 猜图客户端:接收到服务器传的base64数据流,作为img标签src属性值,生成图片。

创建项目


1、创建express项目(已确保你安装了node/express)
$  express --view=ejs 项目名
$  npm install
$  cd  项目名
$  node ./bin/www

运行成功后,在浏览器中打开 http://localhost:3000

(默认端口号为3000,可修改)展示效果如下: image


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');
})

这段代码作用如下:

  1. Express 初始化 app 作为 HTTP 服务器的回调函数。
  2. 我们通过传入 http (HTTP 服务器) 对象初始化了 socket.io 的一个实例。
  3. 定义了一个路由 / 来处理首页访问。
  4. 然后监听 connection 事件来接收 sockets, 并将连接信息打印到控制台。
  5. 使 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文件)编写接受与发布绘画数据流逻辑

image

代码展示区


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的参数值做判断
    // 如果当前是usernamelsp,则展示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);
}

页面展示效果

image

githunb地址: https://github.com/Liao640/guessCavas

项目部署地址: http://193.112.106.197:8085/

用户名为lsp为绘画客户端,其他用户登陆为猜图客户端。功能后续更新

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值